Nekochu's picture
merge Kimodo-cpp into MoMask as two tabs
a8e08c2
|
Raw History Blame Contribute Delete
4.87 kB

A newer version of the Gradio SDK is available: 6.30.0

Upgrade
metadata
title: Kimodo cpp + MoMask
emoji: 🕺
colorFrom: pink
colorTo: purple
sdk: gradio
sdk_version: 6.3.0
python_version: '3.10'
app_file: app.py
pinned: false
fullWidth: true
license: other
license_name: nvidia-open-model-license
license_link: >-
  https://www.nvidia.com/en-us/agreements/enterprise-software/nvidia-open-model-license/
short_description: Two CPU text-to-motion models, Kimodo GGML and MoMask ONNX
startup_duration_timeout: 1h
preload_from_hub:
  - LocalAI-io/Kimodo-SOMA-RP-v1.1-GGML models/kimodo-soma-rp-v1.1-f32.gguf
  - LocalAI-io/Kimodo-SOMA-SEED-v1.1-GGML models/kimodo-soma-seed-v1.1-f32.gguf
  - LocalAI-io/Kimodo-G1-RP-v1-GGML models/kimodo-g1-rp-v1-f32.gguf
  - LocalAI-io/Kimodo-G1-SEED-v1-GGML models/kimodo-g1-seed-v1-f32.gguf
  - LocalAI-io/Llama-3-Kimodo-GGML

Kimodo cpp + MoMask

Two text-to-motion models on one free CPU Space, a tab each. Both take a plain English description and return a skeletal animation plus a BVH file for Blender.

This Space merges WeReCooking2/Kimodo-cpp into what was Luminia/momask-codes.

Which tab to use

Kimodo MoMask
Runtime kimodo.cpp, GGML ONNX Runtime
Rigs SOMA 30 joint human, Unitree G1 34 joint robot 22 joint human
Outputs glTF orbit preview, BVH, MP4 MP4, BVH
First run on a new prompt ~2 min, one LLM2Vec encode ~10 s
Repeat prompt sampling only same

MoMask is the one to reach for first because it is roughly an order of magnitude cheaper. Kimodo is worth the wait when you want the robot rig, a longer clip, or its markedly better motion quality.

Kimodo's cost is not the sampling, it is the text encode. It conditions on a 4096 float LLM2Vec embedding produced by a Llama 3 bundle of 35 files, and kmd-encode reopens all of them per call, so one prompt costs one full pass regardless of length. Embeddings are memoised on the prompt text, so re-rolling a seed or changing the frame count afterwards costs nothing extra. MoMask has no equivalent step because CLIP text encoding is inside its ONNX graph.

Keeping two native stacks in one process

The two pipelines do not share anything, and the merge is mostly about making sure they do not stand on each other.

Import order is load-bearing. 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 app.py imports the drawing stack first, before anything native.

MoMask's backends are deferred. torch, onnxruntime and CLIP each bring their own libstdc++ and libgomp, and Kimodo dlopens libkimodo.so together with its own bundled libgomp.so.1 into the same process. momask_core._ensure_backends() holds them back until the MoMask tab is actually used, so a visitor who only generates with Kimodo never loads them at all.

Each pipeline keeps its own weights. Kimodo runs GGUF checkpoints, MoMask runs its own ONNX graphs. Nothing is deduplicated or substituted across them, which keeps any quality complaint attributable to one model.

Two vCPUs run one job at a time, so a second visitor queues. The browser shows a queue position; an API caller does not.

Models

Kimodo, about 1.13 GB each, all preloaded at build time so a wake costs no download. One is resident at a time.

Checkpoint Rig Joints
SOMA RP v1.1 soma30 30
SOMA SEED v1.1 soma30 30
G1 RP v1 g1skel34 34
G1 SEED v1 g1skel34 34

MoMask, about 417 MB of ONNX in this repo: CLIP text encoder 254 MB, mask transformer 56 MB, residual transformer 55 MB, VQ-VAE decoder 46 MB, length estimator 0.5 MB.

Calling it from the API

Two named endpoints, /generate for Kimodo and /momask_generate for MoMask. Read them from /gradio_api/info rather than guessing.

The Space is also an MCP server at /gradio_api/mcp/sse, so an LLM client can call either tab as a tool.

Watch out for one thing on the streamed response. data: null is the heartbeat payload, not a result and not a failure. It arrives every 15 seconds for as long as the job is queued or running, so read the event: line and treat only complete and error as terminal:

event = None
for line in stream:
    if line.startswith("event: "):
        event = line[7:].strip()
    elif line.startswith("data: ") and event in ("complete", "error"):
        payload = line[6:]
        break

POST /gradio_api/call/generate returns 200 with an event id whatever happens next, so judging success by that status is misleading. The outcome is only on the stream.

Credits

NVIDIA Kimodo through kimodo.cpp. MoMask by Chuan Guo et al.