"""Two CPU text-to-motion models in one Space, one tab each. Import order matters here and is not cosmetic. matplotlib's ft2font is a pybind11 extension built against the system libstdc++ and fails to initialise if a differently linked libstdc++ is already resident, so the drawing stack is imported before anything native. Kimodo then dlopens libkimodo.so together with its own bundled libgomp at generate time, while MoMask's torch, onnxruntime and CLIP are held back behind momask_core._ensure_backends() until that tab is actually used. A visitor who only generates with Kimodo never loads them. Each pipeline keeps its own weights. Kimodo runs GGUF checkpoints through the kimodo.cpp GGML port; MoMask runs its own ONNX graphs. Nothing is shared or substituted between them. """ import matplotlib matplotlib.use("Agg") import matplotlib.pyplot as plt # noqa: E402,F401 import gradio as gr # noqa: E402 import kimodo_core as K # noqa: E402 import momask_core as M # noqa: E402 CSS = K.CSS + """ #mm_log textarea { font-family: ui-monospace, monospace; font-size: 0.8em; } .tabs { gap: 0 !important; } """ HEADER = (f"CPU text to motion · **Kimodo** through " f"[kimodo.cpp](https://github.com/localai-org/kimodo.cpp) GGML, " f"**MoMask** through ONNX · {K.CPUS} threads") def momask_generate(text, length, seed): """Generate a 3D skeletal animation from a text description of a motion. Runs MoMask on CPU through ONNX Runtime. Returns a rendered preview and a BVH file for Blender. Far cheaper than the Kimodo tab because there is no LLM2Vec encode: CLIP text encoding is part of the graph. Args: text: Plain English description of the motion, for example "a person walks forward". length: Clip length in seconds. 0 asks the length estimator to pick one. seed: Random seed. The same seed and text reproduce a clip exactly. """ if not (text or "").strip(): raise gr.Error("Describe a motion first, for example " "\"a person walks forward\".") _joints, video_path, bvh_path = M.generate_motion( text, float(length or 0), int(seed) if seed is not None else None) if video_path is None: raise gr.Error("MoMask returned no motion. Try a shorter, plainer " "description, or set Duration above 0.") return video_path, gr.update(value=bvh_path, interactive=bvh_path is not None) def kimodo_example(prompt, model_label, frames, seed): """Examples vary the four columns worth varying; the advanced controls keep their defaults. Delegating to K.generate rather than duplicating it means a cached example is the same code path the Generate button runs. The BVH button is dropped from the yielded tuple on purpose. Caching an example whose outputs include a gr.DownloadButton crashes inside gradio: postprocess_data reaches components/base.py `if self.info:` and DownloadButton has no `info` attribute. Video, skeleton and log cache fine, and the button still works normally from Generate. """ for video, glb, _bvh, status in K.generate( prompt, model_label, int(frames), 20, int(seed), 2.0, True): yield video, glb, status def momask_example(text, length, seed): """Same reason as above: cache the preview, leave the download button out.""" video, _bvh = momask_generate(text, length, seed) return video def kimodo_tab(): """Kimodo's own layout, unchanged apart from living inside a tab.""" with gr.Row(): with gr.Column(scale=4): prompt = gr.Textbox( value="a person runs forward and jumps over an obstacle", label="Describe the motion", lines=1, max_lines=1, placeholder="a person performs a spinning back kick") run = gr.Button( f"Generate ({K._duration(K.ENCODE_S + K.sampling_seconds(120, 20))})", variant="primary") status = gr.Textbox(lines=8, max_lines=K.LOG_LINES + 2, autoscroll=True, show_label=False, container=False, elem_id="log") with gr.Column(scale=3): model_label = gr.Dropdown( choices=[c.label for c in K.CHECKPOINTS], value=K.CHECKPOINTS[0].label, label="Motion model", info="SOMA is a 30 joint human rig, G1 a 34 joint Unitree " "robot. RP and SEED are different checkpoints of each.") frames = gr.Slider(30, 240, value=120, step=10, label=f"Frames ({120 / K.FPS:.1f} s clip)") with gr.Accordion("Advanced", open=False): with gr.Row(): steps = gr.Slider(4, 50, value=20, step=1, scale=3, label="Diffusion steps (20)", info="More steps, cleaner motion.") seed = gr.Number(value=7, precision=0, label="Seed", scale=1) with gr.Row(): text_cfg = gr.Slider(1.0, 5.0, value=2.0, step=0.5, scale=3, label="Guidance") deskate = gr.Checkbox(value=True, scale=1, label="Reduce foot skating") with gr.Column(scale=5): viewer = gr.Model3D(label="Skeleton", interactive=False, height=260) video = gr.Video(label="Preview", autoplay=True, height=180) bvh = gr.DownloadButton("Download BVH for Blender", size="sm", interactive=False) # Defined after the row because Examples needs every input component to exist. gr.Examples( examples=[ ["a person walks forward at a steady pace", K.CHECKPOINTS[0].label, 120, 42], ["a person performs a roundhouse kick", K.CHECKPOINTS[0].label, 120, 123], ["a person sits down on a chair", K.CHECKPOINTS[0].label, 90, 456], ["a person dances happily in place", K.CHECKPOINTS[0].label, 150, 789], ["a person kicks with their right leg", K.CHECKPOINTS[2].label, 120, 101], ], inputs=[prompt, model_label, frames, seed], outputs=[video, viewer, status], fn=kimodo_example, # Lazy is the only sane mode here. Each distinct prompt pays a ~110 s # LLM2Vec encode, so eager would add roughly ten minutes to every build # and every wake, and one failure would take the whole Space down with it. # Lazy caches a row the first time someone clicks it, instantly after. cache_examples=True, cache_mode="lazy", label="Examples") for control in (prompt, frames, steps): control.change(K.control_labels, inputs=[prompt, frames, steps], outputs=[frames, steps, run], queue=False, api_visibility="private") run.click(K.generate, inputs=[prompt, model_label, frames, steps, seed, text_cfg, deskate], outputs=[video, viewer, bvh, status], api_name="generate").then( K.control_labels, inputs=[prompt, frames, steps], outputs=[frames, steps, run], queue=False, api_visibility="private") def momask_tab(): """Same shape as the Kimodo tab so switching tabs does not relayout the page: inputs left, settings middle, outputs right.""" with gr.Row(): with gr.Column(scale=4): text = gr.Textbox(value="a person walks forward", label="Describe the motion", lines=1, max_lines=1, placeholder="a person does a salsa dance") run = gr.Button("Generate (~10 s)", variant="primary") with gr.Column(scale=3): length = gr.Slider(0, 10, value=0, step=0.5, label="Duration (0 = estimate it)", info="Seconds. The length estimator picks one " "from the text when this is 0.") seed = gr.Number(value=42, precision=0, label="Seed") with gr.Column(scale=5): video = gr.Video(label="Preview", autoplay=True, height=260) bvh = gr.DownloadButton("Download BVH for Blender", size="sm", interactive=False) # Defined after the row because Examples needs every input component to exist. gr.Examples( examples=[ ["A person walks forward", 0, 42], ["A person is running on a treadmill", 0, 123], ["A person jumps up and then lands", 0, 456], ["A person does a salsa dance", 0, 789], ["A person kicks with their right leg", 0, 101], ], inputs=[text, length, seed], outputs=[video], fn=momask_example, # Lazy, not eager. Eager runs every row before the Space serves its first # request, and it would force torch, onnxruntime and CLIP resident at # startup for every visitor, which is exactly what _ensure_backends() # exists to avoid. Lazy caches each row the first time it is clicked and # serves it instantly to everyone after that. cache_examples=True, cache_mode="lazy", label="Examples") run.click(momask_generate, inputs=[text, length, seed], outputs=[video, bvh], api_name="momask_generate") with gr.Blocks(title="Kimodo cpp + MoMask", fill_width=True) as demo: gr.Markdown(HEADER, elem_id="foot") with gr.Tabs(): with gr.Tab("Kimodo (GGML)"): kimodo_tab() with gr.Tab("MoMask (ONNX)"): momask_tab() if __name__ == "__main__": demo.queue(max_size=8, default_concurrency_limit=1).launch( css=CSS, mcp_server=True)