Add SOMA and G1 model support

This commit is contained in:
Richard Palethorpe 2026-08-26 15:59:58 +01:00
parent dedbe1066a
commit 92341f3194
35 changed files with 872 additions and 327 deletions

View File

@ -4,14 +4,24 @@ GGML/C++ implementation of NVIDIA's Kimodo text-to-motion model.
## Status ## Status
`Kimodo-SMPLX-RP-v1` accepts either a UTF-8 prompt or a precomputed LLM2Vec The five released Kimodo motion checkpoints accept either a UTF-8 prompt or a
embedding and generates unconstrained SMPL-X22 local rotations and root precomputed LLM2Vec embedding and generate local rotations plus root
translations on CPU or Vulkan. The text encoder uses eight-layer Vulkan chunks translations on CPU or Vulkan:
by default; set `KIMODO_TEXT_LAYER_CHUNK=1..32` to tune VRAM use.
- SMPL-X RP v1: 22 joints
- SOMA RP/SEED v1.1: the predicted compact 30-joint control skeleton
- G1 RP/SEED v1: 34 Unitree G1 joints
NVIDIA's Python API expands SOMA's predicted 30 joints to a relaxed-hand
77-joint presentation skeleton. The native API currently returns the 30 joints
the model actually predicts. The text encoder uses eight-layer Vulkan chunks by
default; set `KIMODO_TEXT_LAYER_CHUNK=1..32` to tune VRAM use.
Included: checked GGUF loading, safetensors conversion, DDIM sampling, C/C++ Included: checked GGUF loading, safetensors conversion, DDIM sampling, C/C++
APIs, CPU/Vulkan parity tests, and a local text-to-motion demo. Constraints, APIs, conditioned multi-prompt transitions, CPU/Vulkan parity tests,
SOMA, G1, GLB export, and quantised models are not implemented yet. skeleton-only GLB export, and a local text-to-motion demo. General constraint
input, 77-joint SOMA expansion, skinned-mesh GLB export, and quantised models
are not implemented yet.
## Build and test on Linux ## Build and test on Linux
@ -21,7 +31,7 @@ support. GGML is a pinned Git submodule:
```sh ```sh
git submodule update --init --recursive git submodule update --init --recursive
scripts/download_gguf_weights.sh --output "$PWD" scripts/download_gguf_weights.sh --output "$PWD" --model soma-rp-v1.1
cmake --preset debug cmake --preset debug
cmake --build --preset debug cmake --build --preset debug
ctest --preset debug ctest --preset debug
@ -56,8 +66,9 @@ to the process. The GGUF parser fuzzer requires Clang.
`include/kimodo/kimodo_capi.h` is the C API. Model loading checks the motion `include/kimodo/kimodo_capi.h` is the C API. Model loading checks the motion
GGUF and text bundle before inference. Use `kimodo_generate_embedding` for GGUF and text bundle before inference. Use `kimodo_generate_embedding` for
4096 F32 values or `kimodo_generate` for text. Both return SMPL-X22 root 4096 F32 values or `kimodo_generate` for text. Both return the selected model's
translations and local XYZW rotations. root translations and local XYZW rotations; query the joint count from the
result rather than assuming a fixed skeleton.
## Demo ## Demo
@ -69,27 +80,43 @@ go run ./demo -addr 0.0.0.0:8094
Open `http://localhost:8094`. The left sidebar contains the prompt and a Open `http://localhost:8094`. The left sidebar contains the prompt and a
persistent history; choosing a previous animation restores its prompt for a persistent history; choosing a previous animation restores its prompt for a
new generation. new generation. Every successful animation also writes a standalone
`animation.glb` beside its raw streams, for example
`demo-output/<animation-id>/animation.glb`. It contains the selected animated
node hierarchy (no mesh), ready to copy into a Three.js project. It is also
available from `/api/animations/<animation-id>/animation.glb` while the demo
is running.
## Weights ## Weights
Ready-to-run native GGML weights are published under the Hugging Face Ready-to-run native GGML weights are published under the Hugging Face
`LocalAI-io` organisation (not GitHub's `localai-org`). The reusable `LocalAI-io` organisation (not GitHub's `localai-org`). The reusable
[Llama-3-Kimodo-GGML](https://huggingface.co/LocalAI-io/Llama-3-Kimodo-GGML) [Llama-3-Kimodo-GGML](https://huggingface.co/LocalAI-io/Llama-3-Kimodo-GGML)
text encoder and the upstream-linked text encoder is separate from the four redistributable motion repositories,
[Kimodo-SMPLX-RP-v1-GGML](https://huggingface.co/LocalAI-io/Kimodo-SMPLX-RP-v1-GGML) each of which preserves a one-to-one relationship to its NVIDIA upstream:
diffusion model are separate, so users download rather than recreate them:
- [Kimodo-SOMA-RP-v1.1-GGML](https://huggingface.co/LocalAI-io/Kimodo-SOMA-RP-v1.1-GGML)
- [Kimodo-SOMA-SEED-v1.1-GGML](https://huggingface.co/LocalAI-io/Kimodo-SOMA-SEED-v1.1-GGML)
- [Kimodo-G1-RP-v1-GGML](https://huggingface.co/LocalAI-io/Kimodo-G1-RP-v1-GGML)
- [Kimodo-G1-SEED-v1-GGML](https://huggingface.co/LocalAI-io/Kimodo-G1-SEED-v1-GGML)
Download one or repeat `--model` to install several:
```sh ```sh
scripts/download_gguf_weights.sh --output "$PWD" scripts/download_gguf_weights.sh --output "$PWD" \
--model soma-rp-v1.1 --model g1-rp-v1
``` ```
The installer verifies each published manifest and SHA-256 hashes. Use The installer verifies each published manifest and SHA-256 hashes. Use
`--motion-only` when supplying a precomputed 4096-float LLM2Vec embedding. `--motion-only` when supplying a precomputed 4096-float LLM2Vec embedding.
SMPL-X RP is deliberately absent from the published-weight installer: its
internal-R&D licence prohibits distributing derivative models, so it must be
converted locally after the user obtains the upstream checkpoint under its
gated terms.
The GGUF bundle includes converted Meta Llama 3 material and Kimodo is The text bundle includes converted Meta Llama 3 material and retains its
non-commercial research-only. Review the published model card and upstream separate terms. Review every selected model card before downloading or
licences before downloading or redistributing. redistributing.
## License ## License
@ -97,6 +124,22 @@ The C++ port and its original tooling are licensed under Apache-2.0; see
[LICENSE](LICENSE). GGML and the model weights retain their respective [LICENSE](LICENSE). GGML and the model weights retain their respective
licences. licences.
| Motion checkpoint | Upstream terms | Commercial use |
| --- | --- | --- |
| Kimodo-SMPLX-RP-v1 | [NVIDIA Internal Scientific Research and Development Model License](https://www.nvidia.com/en-us/agreements/enterprise-software/nvidia-internal-scientific-research-and-development-model-license/) | No; internal, non-production R&D only; derivative model redistribution is prohibited |
| SOMA RP/SEED v1.1 | [NVIDIA Open Model License](https://www.nvidia.com/en-us/agreements/enterprise-software/nvidia-open-model-license/) | Permitted by the model licence |
| G1 RP/SEED v1 | [NVIDIA Open Model License](https://www.nvidia.com/en-us/agreements/enterprise-software/nvidia-open-model-license/) | Permitted by the model licence |
The SMPL-X warning is about NVIDIA's trained Kimodo checkpoint, not the mere
fact that its output uses an SMPL-X-shaped 22-joint hierarchy. Converting that
checkpoint to GGUF is a new runtime representation of the same weights and does
not replace its licence. Skeleton names, parent links, and the Apache-2.0 port
source do not by themselves make the SOMA or G1 checkpoints non-commercial.
The SMPL-X Hugging Face metadata, model card, and access terms identify the
internal-R&D licence; treat those restrictive terms as controlling even though
an apparently inconsistent `LICENSE` file has also appeared in that upstream
repository.
### Regenerating the bundle ### Regenerating the bundle
This is only needed to reproduce a conversion. The SMPL-X checkpoint and Llama This is only needed to reproduce a conversion. The SMPL-X checkpoint and Llama
@ -105,8 +148,9 @@ authenticating, download the exact revisions and hash manifests with:
```sh ```sh
nix develop path:. --command hf auth login nix develop path:. --command hf auth login
nix develop path:. --command scripts/download_weights.sh \ scripts/download_weights.sh --output "$PWD/models" --with-text \
--output "$PWD/models" --with-text --model smplx-rp-v1 --model soma-rp-v1.1 --model soma-seed-v1.1 \
--model g1-rp-v1 --model g1-seed-v1
``` ```
Convert the local LLM2Vec model to the native component bundle with: Convert the local LLM2Vec model to the native component bundle with:
@ -120,9 +164,10 @@ Validate a prospective release without network access, then explicitly upload
it from an account allowed to publish to `LocalAI-io`: it from an account allowed to publish to `LocalAI-io`:
```sh ```sh
nix develop path:. --command python scripts/publish_gguf.py --component motion
nix develop path:. --command python scripts/publish_gguf.py --component motion \ nix develop path:. --command python scripts/publish_gguf.py --component motion \
--upload --confirm-upstream-licences --motion-model soma-rp-v1.1
nix develop path:. --command python scripts/publish_gguf.py --component motion \
--motion-model soma-rp-v1.1 --upload --confirm-upstream-licences
nix develop path:. --command python scripts/publish_gguf.py --component text \ nix develop path:. --command python scripts/publish_gguf.py --component text \
--upload --confirm-upstream-licences --upload --confirm-upstream-licences
``` ```

View File

@ -1,20 +1,21 @@
<!doctype html><html lang="en"><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"><title>Kimodo text to motion</title> <!doctype html><html lang="en"><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"><title>Kimodo text to motion</title>
<style> <style>
:root{color-scheme:dark;font-family:Inter,system-ui,sans-serif;background:#10131b;color:#f2f5fb}body{margin:0;overflow:hidden;background:#10131b}main{width:100vw;height:100vh;display:grid;grid-template-columns:360px minmax(0,1fr)}.sidebar{padding:24px 18px;display:flex;flex-direction:column;gap:18px;overflow-y:auto;overflow-x:hidden;background:radial-gradient(circle at 0 0,#293b62,transparent 32rem),#141a27;border-right:1px solid #2b354b}h1{font-size:2rem;letter-spacing:-.05em;margin:0}.eyebrow{font-size:.7rem;text-transform:uppercase;letter-spacing:.14em;color:#9bb8ff;margin-bottom:8px}.card{background:#171c28dd;border:1px solid #2b354b;border-radius:14px;overflow:hidden}.stage{min-width:0;min-height:0;display:flex;flex-direction:column;background:#0c1019}canvas{display:block;width:100%;height:100%;flex:1;min-height:0;background:linear-gradient(#111b31,#0c1019);cursor:grab;touch-action:none}canvas.dragging{cursor:grabbing}.controls{padding:14px 18px;display:flex;align-items:center;gap:10px;flex-wrap:wrap;border-top:1px solid #2b354b}.promptbox{padding:16px;display:grid;gap:10px}.promptbox textarea{min-height:110px;resize:none;overflow:hidden}textarea,input{box-sizing:border-box;width:100%;border:1px solid #35415b;border-radius:10px;background:#0d121d;color:#f2f5fb;padding:10px;font:inherit}button{border:0;border-radius:999px;padding:10px 16px;background:#9bb8ff;color:#10131b;font:inherit;font-weight:700;cursor:pointer}button:disabled{opacity:.5;cursor:wait}.readout,.hint{color:#a9b3c8;font-size:.88rem}.gallery{padding:4px;display:grid;gap:8px;align-content:start}.history{flex:none;min-height:0;overflow:visible}.gallery h2{font-size:1rem;margin:8px}.item{width:100%;background:#101622;border:1px solid #2c3750;border-radius:12px;padding:11px;text-align:left;color:#e8eefc}.item:hover,.item.active{border-color:#9bb8ff;background:#19233a}.item p{margin:0 0 6px;font-size:.88rem;line-height:1.35}.status{font-size:.75rem;color:#a9b3c8}.error{color:#ffacac;display:-webkit-box;-webkit-line-clamp:3;-webkit-box-orient:vertical;overflow:hidden}.camera{flex:1 1 100%;color:#a9b3c8;font-size:.82rem}.progress{display:inline-flex;align-items:center;gap:7px;color:#d9e5ff;font-weight:600}.progress::before{content:'';width:8px;height:8px;border-radius:50%;background:#9bb8ff;box-shadow:0 0 0 0 #9bb8ff;animation:pulse 1.25s infinite}@keyframes pulse{70%{box-shadow:0 0 0 8px #9bb8ff00}}.item.running{border-color:#739cf7}@media(max-width:850px){body{overflow:auto}main{height:auto;min-height:100vh;grid-template-columns:1fr}.sidebar{overflow:visible}.stage{height:min(70vh,700px)}} :root{color-scheme:dark;font-family:Inter,system-ui,sans-serif;background:#10131b;color:#f2f5fb}body{margin:0;overflow:hidden;background:#10131b}main{width:100vw;height:100vh;display:grid;grid-template-columns:360px minmax(0,1fr)}.sidebar{padding:24px 18px;display:flex;flex-direction:column;gap:18px;overflow-y:auto;overflow-x:hidden;background:radial-gradient(circle at 0 0,#293b62,transparent 32rem),#141a27;border-right:1px solid #2b354b}h1{font-size:2rem;letter-spacing:-.05em;margin:0}.eyebrow{font-size:.7rem;text-transform:uppercase;letter-spacing:.14em;color:#9bb8ff;margin-bottom:8px}.card{background:#171c28dd;border:1px solid #2b354b;border-radius:14px;overflow:hidden}.stage{min-width:0;min-height:0;display:flex;flex-direction:column;background:#0c1019}canvas{display:block;width:100%;height:100%;flex:1;min-height:0;background:linear-gradient(#111b31,#0c1019);cursor:grab;touch-action:none}canvas.dragging{cursor:grabbing}.controls{padding:14px 18px;display:flex;align-items:center;gap:10px;flex-wrap:wrap;border-top:1px solid #2b354b}.promptbox{padding:16px;display:grid;gap:10px}.promptbox textarea{min-height:110px;resize:none;overflow:hidden}textarea,input{box-sizing:border-box;width:100%;border:1px solid #35415b;border-radius:10px;background:#0d121d;color:#f2f5fb;padding:10px;font:inherit}button,.download{border:0;border-radius:999px;padding:10px 16px;background:#9bb8ff;color:#10131b;font:inherit;font-weight:700;cursor:pointer;text-decoration:none}button:disabled{opacity:.5;cursor:wait}.download{background:#24313a;color:#dce9e8}.readout,.hint{color:#a9b3c8;font-size:.88rem}.gallery{padding:4px;display:grid;gap:8px;align-content:start}.history{flex:none;min-height:0;overflow:visible}.gallery h2{font-size:1rem;margin:8px}.item{width:100%;background:#101622;border:1px solid #2c3750;border-radius:12px;padding:11px;text-align:left;color:#e8eefc}.item:hover,.item.active{border-color:#9bb8ff;background:#19233a}.item p{margin:0 0 6px;font-size:.88rem;line-height:1.35}.status{font-size:.75rem;color:#a9b3c8}.error{color:#ffacac;display:-webkit-box;-webkit-line-clamp:3;-webkit-box-orient:vertical;overflow:hidden}.camera{flex:1 1 100%;color:#a9b3c8;font-size:.82rem}.progress{display:inline-flex;align-items:center;gap:7px;color:#d9e5ff;font-weight:600}.progress::before{content:'';width:8px;height:8px;border-radius:50%;background:#9bb8ff;box-shadow:0 0 0 0 #9bb8ff;animation:pulse 1.25s infinite}@keyframes pulse{70%{box-shadow:0 0 0 8px #9bb8ff00}}.item.running{border-color:#739cf7}@media(max-width:850px){body{overflow:auto}main{height:auto;min-height:100vh;grid-template-columns:1fr}.sidebar{overflow:visible}.stage{height:min(70vh,700px)}}
/* LocalAI house style: dark graphite surfaces with a cyan/violet edge. */ /* LocalAI house style: dark graphite surfaces with a cyan/violet edge. */
:root{--la-bg:#080b0f;--la-panel:#10151b;--la-panel-2:#151c23;--la-line:#25313b;--la-text:#f3f6f4;--la-muted:#8b9aa5;--la-cyan:#3bd3d0;--la-cyan-2:#5ee6be;--la-violet:#9f7aea}body{background:var(--la-bg);color:var(--la-text)}.sidebar{background:radial-gradient(circle at 18% 0,rgba(59,211,208,.12),transparent 26rem),var(--la-panel);border-color:var(--la-line);box-sizing:border-box;min-height:0;min-width:0}.card{background:var(--la-panel-2);border-color:var(--la-line)}.stage{background:#090d12}canvas{background:radial-gradient(circle at 50% 25%,#102632 0,#090d12 56%)}.eyebrow{color:var(--la-cyan);font-family:ui-monospace,SFMono-Regular,Menlo,monospace}.product-title{display:flex;align-items:center;gap:11px}.product-mark{display:grid;place-items:center;width:36px;height:36px;border-radius:11px;background:linear-gradient(135deg,var(--la-cyan),#218eac 55%,var(--la-violet));color:#051013;font-size:20px;font-weight:900;box-shadow:0 0 28px rgba(59,211,208,.2)}.localai-logo{position:fixed;top:18px;right:18px;z-index:2;width:56px;height:56px;object-fit:contain;filter:drop-shadow(0 5px 16px #0009)}.promptbox label,.gallery h2{color:var(--la-text)}textarea,input{border-color:var(--la-line);background:#0b1015}button{background:linear-gradient(135deg,var(--la-cyan),var(--la-cyan-2));color:#051013;border-radius:8px;box-shadow:0 8px 22px rgba(59,211,208,.12)}.history,.gallery,#items{min-width:0}.item{background:#0b1015;border-color:var(--la-line);box-sizing:border-box;min-width:0;white-space:normal;overflow-wrap:anywhere}.item:hover,.item.active{border-color:var(--la-cyan);background:#111d24}.status,.readout,.hint,.camera{color:var(--la-muted)}.progress{color:var(--la-cyan-2)}.progress::before{background:var(--la-cyan);box-shadow:0 0 0 0 var(--la-cyan)}.item.running{border-color:var(--la-cyan)} :root{--la-bg:#080b0f;--la-panel:#10151b;--la-panel-2:#151c23;--la-line:#25313b;--la-text:#f3f6f4;--la-muted:#8b9aa5;--la-cyan:#3bd3d0;--la-cyan-2:#5ee6be;--la-violet:#9f7aea}body{background:var(--la-bg);color:var(--la-text)}.sidebar{background:radial-gradient(circle at 18% 0,rgba(59,211,208,.12),transparent 26rem),var(--la-panel);border-color:var(--la-line);box-sizing:border-box;min-height:0;min-width:0}.card{background:var(--la-panel-2);border-color:var(--la-line)}.stage{background:#090d12}canvas{background:radial-gradient(circle at 50% 25%,#102632 0,#090d12 56%)}.eyebrow{color:var(--la-cyan);font-family:ui-monospace,SFMono-Regular,Menlo,monospace}.product-title{display:flex;align-items:center;gap:11px}.product-mark{display:grid;place-items:center;width:36px;height:36px;border-radius:11px;background:linear-gradient(135deg,var(--la-cyan),#218eac 55%,var(--la-violet));color:#051013;font-size:20px;font-weight:900;box-shadow:0 0 28px rgba(59,211,208,.2)}.localai-logo{position:fixed;top:18px;right:18px;z-index:2;width:56px;height:56px;object-fit:contain;filter:drop-shadow(0 5px 16px #0009)}.promptbox label,.gallery h2{color:var(--la-text)}textarea,input{border-color:var(--la-line);background:#0b1015}button{background:linear-gradient(135deg,var(--la-cyan),var(--la-cyan-2));color:#051013;border-radius:8px;box-shadow:0 8px 22px rgba(59,211,208,.12)}.history,.gallery,#items{min-width:0}.item{background:#0b1015;border-color:var(--la-line);box-sizing:border-box;min-width:0;white-space:normal;overflow-wrap:anywhere}.item:hover,.item.active{border-color:var(--la-cyan);background:#111d24}.status,.readout,.hint,.camera{color:var(--la-muted)}.progress{color:var(--la-cyan-2)}.progress::before{background:var(--la-cyan);box-shadow:0 0 0 0 var(--la-cyan)}.item.running{border-color:var(--la-cyan)}
</style><main><aside class="sidebar"><header><div class="eyebrow">Kimodo-SMPLX-RP-v1 · Vulkan</div><div class="product-title"><span class="product-mark">K</span><h1>Text to motion</h1></div></header><section class="card promptbox"><label for="prompt">Describe a motion</label><textarea id="prompt">A person runs forward and then leaps over an obstacle in front of them.</textarea><button id="generate">Generate motion</button><span id="status" class="readout">Ready</span><div class="hint">One generation runs at a time. Select an animation below to restore and edit its prompt.</div></section><section class="card history"><div class="gallery"><h2>Past animations</h2><div id="items" class="readout">Loading…</div></div></section></aside><section class="stage"><canvas id="view" width="1280" height="720" aria-label="Animated SMPL-X skeleton"></canvas><div class="controls"><button id="play">Pause</button><button id="reset">Reset view</button><input id="frame" type="range" min="0" value="0" step="1"><span id="frameText" class="readout"></span><span class="camera">Camera tracks motion · Drag: rotate · wheel: zoom · Shift/right-drag: pan · double-click: reset</span></div></section></main><img class="localai-logo" src="/localai.png" alt="LocalAI"> </style><main><aside class="sidebar"><header><div class="eyebrow">Kimodo-SMPLX-RP-v1 · Vulkan</div><div class="product-title"><span class="product-mark">K</span><h1>Text to motion</h1></div></header><section class="card promptbox"><label for="prompt">Describe a motion</label><textarea id="prompt">A person runs forward and then leaps over an obstacle in front of them.</textarea><button id="generate">Generate motion</button><span id="status" class="readout">Ready</span><div class="hint">One generation runs at a time. Select an animation below to restore and edit its prompt.</div></section><section class="card history"><div class="gallery"><h2>Past animations</h2><div id="items" class="readout">Loading…</div></div></section></aside><section class="stage"><canvas id="view" width="1280" height="720" aria-label="Animated SMPL-X skeleton"></canvas><div class="controls"><button id="play">Pause</button><button id="reset">Reset view</button><a id="download" class="download" hidden download>Download GLB</a><input id="frame" type="range" min="0" value="0" step="1"><span id="frameText" class="readout"></span><span class="camera">Camera tracks motion · Drag: rotate · wheel: zoom · Shift/right-drag: pan · double-click: reset</span></div></section></main><img class="localai-logo" src="/localai.png" alt="LocalAI">
<style>.promptbox,.history{flex:0 0 auto}</style> <style>.promptbox,.history{flex:0 0 auto}.license-warning{color:#ffbf78!important;font-weight:650}</style>
<script src="/models.js"></script><script> <script src="/models.js"></script><script>
const parents=[-1,0,0,0,1,2,3,4,5,6,7,8,9,9,9,12,13,14,16,17,18,19]; let parents=[-1,0,0,0,1,2,3,4,5,6,7,8,9,9,9,12,13,14,16,17,18,19];
// SMPL-X22 rest offsets calibrated from the captured upstream posed-joint / // SMPL-X22 rest offsets calibrated from the captured upstream posed-joint /
// global-rotation fixture. The generator persists local rotations and root // global-rotation fixture. The generator persists local rotations and root
// translations; these fixed parent-local bone vectors make them viewable here. // translations; these fixed parent-local bone vectors make them viewable here.
const offsets=[[0,0,0],[.052299,-.093936,-.027607],[-.057193,-.106548,-.022218],[-.001496,.11293,-.024981],[.058867,-.416442,-.006557],[-.048074,-.39756,-.014061],[.0069,.145636,-.006859],[-.041738,-.437584,-.029512],[.014489,-.446853,-.01803],[-.010334,.056082,.021116],[.049294,-.065279,.126259],[-.040575,-.065287,.127076],[-.011026,.171365,-.028827],[.047725,.087643,-.008375],[-.046636,.086612,-.014864],[.024654,.175391,.024463],[.126285,.05768,-.013885],[-.109342,.053674,-.009118],[.272907,-.069853,-.039094],[-.292029,-.03544,-.024565],[.276174,.021254,-.002478],[-.271878,-.004835,-.016445]]; let offsets=[[0,0,0],[.052299,-.093936,-.027607],[-.057193,-.106548,-.022218],[-.001496,.11293,-.024981],[.058867,-.416442,-.006557],[-.048074,-.39756,-.014061],[.0069,.145636,-.006859],[-.041738,-.437584,-.029512],[.014489,-.446853,-.01803],[-.010334,.056082,.021116],[.049294,-.065279,.126259],[-.040575,-.065287,.127076],[-.011026,.171365,-.028827],[.047725,.087643,-.008375],[-.046636,.086612,-.014864],[.024654,.175391,.024463],[.126285,.05768,-.013885],[-.109342,.053674,-.009118],[.272907,-.069853,-.039094],[-.292029,-.03544,-.024565],[.276174,.021254,-.002478],[-.271878,-.004835,-.016445]];
const canvas=document.querySelector('#view'),ctx=canvas.getContext('2d'),slider=document.querySelector('#frame'),promptBox=document.querySelector('#prompt'),generate=document.querySelector('#generate'),status=document.querySelector('#status'),items=document.querySelector('#items');let selected,root,rotations,frame=0,playbackBlend=0,playing=true,last=0,cameraLast=0,animations=[],activeRequest,activeStarted;const defaultView={yaw:-.68,pitch:-.20,zoom:2700,panX:0,panY:0,cameraX:0,cameraZ:0,cameraZoom:2700,cameraSpeed:0},view={...defaultView};window.addEventListener('kimodo:sequence-controls-ready',()=>{if(selected)window.dispatchEvent(new CustomEvent('kimodo:restore-sequence',{detail:{segments:selected.segments,model:selected.model}}))}); window.addEventListener('kimodo:restore-sequence',event=>{const id=event.detail?.model||'smplx-rp-v1',model=window.kimodoModels?.find(item=>item.id===id);if(model?.parents?.length&&model.offsets?.length===model.parents.length){parents=model.parents;offsets=model.offsets}});
const canvas=document.querySelector('#view'),ctx=canvas.getContext('2d'),slider=document.querySelector('#frame'),promptBox=document.querySelector('#prompt'),generate=document.querySelector('#generate'),status=document.querySelector('#status'),items=document.querySelector('#items'),download=document.querySelector('#download');let selected,root,rotations,frame=0,playbackBlend=0,playing=true,last=0,cameraLast=0,animations=[],activeRequest,activeStarted;const defaultView={yaw:-.68,pitch:-.20,zoom:2700,panX:0,panY:0,cameraX:0,cameraZ:0,cameraZoom:2700,cameraSpeed:0},view={...defaultView};window.addEventListener('kimodo:sequence-controls-ready',()=>{if(selected)window.dispatchEvent(new CustomEvent('kimodo:restore-sequence',{detail:{segments:selected.segments,model:selected.model}}))});
function renderedRoot(){if(!selected||!root)return[0,0,0];const at=frame*3,next=((frame+1)%selected.frames)*3,b=playbackBlend;return[root[at]*(1-b)+root[next]*b,root[at+1]*(1-b)+root[next+1]*b,root[at+2]*(1-b)+root[next+2]*b]}function snapCamera(){const[x,,z]=renderedRoot();view.cameraX=x;view.cameraZ=z;view.cameraZoom=view.zoom;view.cameraSpeed=0;cameraLast=performance.now()}function reset(){Object.assign(view,defaultView);snapCamera();draw()}function updateCamera(now){if(!selected||!root)return;const dt=Math.min(.1,Math.max(0,(now-cameraLast)/1000));cameraLast=now;const at=frame*3,next=((frame+1)%selected.frames)*3,[rootX,,rootZ]=renderedRoot(),speed=playing?Math.hypot(root[next]-root[at],root[next+2]-root[at+2])*30:0,dx=rootX-view.cameraX,dz=rootZ-view.cameraZ,distance=Math.hypot(dx,dz),deadZone=.18;const targetX=distance>deadZone?rootX-dx/distance*deadZone:view.cameraX,targetZ=distance>deadZone?rootZ-dz/distance*deadZone:view.cameraZ,follow=1-Math.exp(-3*dt),speedFollow=1-Math.exp(-2.4*dt),zoomFollow=1-Math.exp(-2.6*dt);view.cameraSpeed+=(speed-view.cameraSpeed)*speedFollow;const targetZoom=view.zoom*(1-Math.min(.22,view.cameraSpeed*.035));view.cameraX+=(targetX-view.cameraX)*follow;view.cameraZ+=(targetZ-view.cameraZ)*follow;view.cameraZoom+=(targetZoom-view.cameraZoom)*zoomFollow}function rotate(q,v){const[x,y,z,w]=q,[vx,vy,vz]=v,tx=2*(y*vz-z*vy),ty=2*(z*vx-x*vz),tz=2*(x*vy-y*vx);return[vx+w*tx+y*tz-z*ty,vy+w*ty+z*tx-x*tz,vz+w*tz+x*ty-y*tx]};function add(a,b){return[a[0]+b[0],a[1]+b[1],a[2]+b[2]]}function multiply(a,b){const[x,y,z,w]=a,[X,Y,Z,W]=b;return[x*W+w*X+y*Z-z*Y,y*W+w*Y+z*X-x*Z,z*W+w*Z+x*Y-y*X,w*W-x*X-y*Y-z*Z]} function renderedRoot(){if(!selected||!root)return[0,0,0];const at=frame*3,next=((frame+1)%selected.frames)*3,b=playbackBlend;return[root[at]*(1-b)+root[next]*b,root[at+1]*(1-b)+root[next+1]*b,root[at+2]*(1-b)+root[next+2]*b]}function snapCamera(){const[x,,z]=renderedRoot();view.cameraX=x;view.cameraZ=z;view.cameraZoom=view.zoom;view.cameraSpeed=0;cameraLast=performance.now()}function reset(){Object.assign(view,defaultView);snapCamera();draw()}function updateCamera(now){if(!selected||!root)return;const dt=Math.min(.1,Math.max(0,(now-cameraLast)/1000));cameraLast=now;const at=frame*3,next=((frame+1)%selected.frames)*3,[rootX,,rootZ]=renderedRoot(),speed=playing?Math.hypot(root[next]-root[at],root[next+2]-root[at+2])*30:0,dx=rootX-view.cameraX,dz=rootZ-view.cameraZ,distance=Math.hypot(dx,dz),deadZone=.18;const targetX=distance>deadZone?rootX-dx/distance*deadZone:view.cameraX,targetZ=distance>deadZone?rootZ-dz/distance*deadZone:view.cameraZ,follow=1-Math.exp(-3*dt),speedFollow=1-Math.exp(-2.4*dt),zoomFollow=1-Math.exp(-2.6*dt);view.cameraSpeed+=(speed-view.cameraSpeed)*speedFollow;const targetZoom=view.zoom*(1-Math.min(.22,view.cameraSpeed*.035));view.cameraX+=(targetX-view.cameraX)*follow;view.cameraZ+=(targetZ-view.cameraZ)*follow;view.cameraZoom+=(targetZoom-view.cameraZoom)*zoomFollow}function rotate(q,v){const[x,y,z,w]=q,[vx,vy,vz]=v,tx=2*(y*vz-z*vy),ty=2*(z*vx-x*vz),tz=2*(x*vy-y*vx);return[vx+w*tx+y*tz-z*ty,vy+w*ty+z*tx-x*tz,vz+w*tz+x*ty-y*tx]};function add(a,b){return[a[0]+b[0],a[1]+b[1],a[2]+b[2]]}function multiply(a,b){const[x,y,z,w]=a,[X,Y,Z,W]=b;return[x*W+w*X+y*Z-z*Y,y*W+w*Y+z*X-x*Z,z*W+w*Z+x*Y-y*X,w*W-x*X-y*Y-z*Z]}
function pose(){if(!selected||!root||!rotations)return[];const positions=[],global=[],rootPosition=renderedRoot();for(let j=0;j<22;j++){const q=Array.from(rotations.subarray((frame*22+j)*4,(frame*22+j+1)*4)),p=parents[j];if(p<0){global[j]=q;positions[j]=rootPosition}else{global[j]=multiply(global[p],q);positions[j]=add(positions[p],rotate(global[p],offsets[j]))}}return positions}function project([x,y,z]){x-=view.cameraX;z-=view.cameraZ;const rx=x*Math.cos(view.yaw)-z*Math.sin(view.yaw),rz=x*Math.sin(view.yaw)+z*Math.cos(view.yaw),ry=y*Math.cos(view.pitch)-rz*Math.sin(view.pitch),dz=y*Math.sin(view.pitch)+rz*Math.cos(view.pitch)+8;return[canvas.width/2+view.panX+view.cameraZoom*rx/dz,canvas.height*.78+view.panY-view.cameraZoom*ry/dz]} function pose(){if(!selected||!root||!rotations)return[];const positions=[],global=[],rootPosition=renderedRoot(),joints=parents.length;for(let j=0;j<joints;j++){const q=Array.from(rotations.subarray((frame*joints+j)*4,(frame*joints+j+1)*4)),p=parents[j];if(p<0){global[j]=q;positions[j]=rootPosition}else{global[j]=multiply(global[p],q);positions[j]=add(positions[p],rotate(global[p],offsets[j]))}}return positions}function project([x,y,z]){x-=view.cameraX;z-=view.cameraZ;const rx=x*Math.cos(view.yaw)-z*Math.sin(view.yaw),rz=x*Math.sin(view.yaw)+z*Math.cos(view.yaw),ry=y*Math.cos(view.pitch)-rz*Math.sin(view.pitch),dz=y*Math.sin(view.pitch)+rz*Math.cos(view.pitch)+8;return[canvas.width/2+view.panX+view.cameraZoom*rx/dz,canvas.height*.78+view.panY-view.cameraZoom*ry/dz]}
function ground(){const cx=0,cz=0,step=.5,initialExtent=6,drawGrid=(extent,alpha,colour,width)=>{ctx.globalAlpha=alpha;ctx.strokeStyle=colour;ctx.lineWidth=width;for(let i=-extent/step;i<=extent/step;i++){let a=project([cx-extent,0,cz+i*step]),b=project([cx+extent,0,cz+i*step]);ctx.beginPath();ctx.moveTo(a[0],a[1]);ctx.lineTo(b[0],b[1]);ctx.stroke();a=project([cx+i*step,0,cz-extent]),b=project([cx+i*step,0,cz+extent]);ctx.beginPath();ctx.moveTo(a[0],a[1]);ctx.lineTo(b[0],b[1]);ctx.stroke()}};const[x,,z]=renderedRoot(),rootDistance=Math.max(Math.abs(x),Math.abs(z)),outerAlpha=.72*Math.max(0,Math.min(1,(rootDistance-3.5)/2.5));if(outerAlpha)drawGrid(18,outerAlpha,'#173943',1);drawGrid(initialExtent,1,'#173943',2);ctx.globalAlpha=1;ctx.strokeStyle='#23606b';ctx.lineWidth=3;const xAxis=project([cx-initialExtent,0,cz]),zAxis=project([cx,0,cz-initialExtent]),xEnd=project([cx+initialExtent,0,cz]),zEnd=project([cx,0,cz+initialExtent]);ctx.beginPath();ctx.moveTo(xAxis[0],xAxis[1]);ctx.lineTo(xEnd[0],xEnd[1]);ctx.moveTo(zAxis[0],zAxis[1]);ctx.lineTo(zEnd[0],zEnd[1]);ctx.stroke()}function draw(){ctx.clearRect(0,0,canvas.width,canvas.height);const p=pose();ground();if(p.length){ctx.lineCap='round';for(let pass=0;pass<2;pass++){ctx.strokeStyle=pass?'#65eee1':'#071014';ctx.lineWidth=pass?7:13;for(let i=1;i<22;i++){const a=project(p[i]),b=project(p[parents[i]]);ctx.beginPath();ctx.moveTo(a[0],a[1]);ctx.lineTo(b[0],b[1]);ctx.stroke()}}for(const x of p){const a=project(x);ctx.fillStyle='#071014';ctx.beginPath();ctx.arc(a[0],a[1],7,0,Math.PI*2);ctx.fill();ctx.fillStyle='#ecfffd';ctx.beginPath();ctx.arc(a[0],a[1],4,0,Math.PI*2);ctx.fill()}}slider.value=frame;document.querySelector('#frameText').textContent=selected?`frame ${frame+1} / ${selected.frames}`:'No animation selected'}function tick(t){if(selected){const interval=1000/30;if(!last)last=t;if(playing){while(t-last>=interval){frame=(frame+1)%selected.frames;last+=interval}playbackBlend=Math.min(1,(t-last)/interval)}else playbackBlend=0;updateCamera(t);draw()}requestAnimationFrame(tick)} function ground(){const cx=0,cz=0,step=.5,initialExtent=6,drawGrid=(extent,alpha,colour,width)=>{ctx.globalAlpha=alpha;ctx.strokeStyle=colour;ctx.lineWidth=width;for(let i=-extent/step;i<=extent/step;i++){let a=project([cx-extent,0,cz+i*step]),b=project([cx+extent,0,cz+i*step]);ctx.beginPath();ctx.moveTo(a[0],a[1]);ctx.lineTo(b[0],b[1]);ctx.stroke();a=project([cx+i*step,0,cz-extent]),b=project([cx+i*step,0,cz+extent]);ctx.beginPath();ctx.moveTo(a[0],a[1]);ctx.lineTo(b[0],b[1]);ctx.stroke()}};const[x,,z]=renderedRoot(),rootDistance=Math.max(Math.abs(x),Math.abs(z)),outerAlpha=.72*Math.max(0,Math.min(1,(rootDistance-3.5)/2.5));if(outerAlpha)drawGrid(18,outerAlpha,'#173943',1);drawGrid(initialExtent,1,'#173943',2);ctx.globalAlpha=1;ctx.strokeStyle='#23606b';ctx.lineWidth=3;const xAxis=project([cx-initialExtent,0,cz]),zAxis=project([cx,0,cz-initialExtent]),xEnd=project([cx+initialExtent,0,cz]),zEnd=project([cx,0,cz+initialExtent]);ctx.beginPath();ctx.moveTo(xAxis[0],xAxis[1]);ctx.lineTo(xEnd[0],xEnd[1]);ctx.moveTo(zAxis[0],zAxis[1]);ctx.lineTo(zEnd[0],zEnd[1]);ctx.stroke()}function draw(){ctx.clearRect(0,0,canvas.width,canvas.height);const p=pose();ground();if(p.length){ctx.lineCap='round';for(let pass=0;pass<2;pass++){ctx.strokeStyle=pass?'#65eee1':'#071014';ctx.lineWidth=pass?7:13;for(let i=1;i<parents.length;i++){const a=project(p[i]),b=project(p[parents[i]]);ctx.beginPath();ctx.moveTo(a[0],a[1]);ctx.lineTo(b[0],b[1]);ctx.stroke()}}for(const x of p){const a=project(x);ctx.fillStyle='#071014';ctx.beginPath();ctx.arc(a[0],a[1],7,0,Math.PI*2);ctx.fill();ctx.fillStyle='#ecfffd';ctx.beginPath();ctx.arc(a[0],a[1],4,0,Math.PI*2);ctx.fill()}}slider.value=frame;document.querySelector('#frameText').textContent=selected?`frame ${frame+1} / ${selected.frames}`:'No animation selected'}function tick(t){if(selected){const interval=1000/30;if(!last)last=t;if(playing){while(t-last>=interval){frame=(frame+1)%selected.frames;last+=interval}playbackBlend=Math.min(1,(t-last)/interval)}else playbackBlend=0;updateCamera(t);draw()}requestAnimationFrame(tick)}
async function select(a){if(a.status!=='ready')return;selected=a;promptBox.value=a.prompt;window.dispatchEvent(new CustomEvent('kimodo:restore-sequence',{detail:{segments:a.segments,model:a.model}}));status.className='readout';status.textContent=`Selected ${a.id.slice(0,8)} · sequence restored`;[root,rotations]=await Promise.all([fetch(`/api/animations/${a.id}/root.f32`).then(r=>r.arrayBuffer()).then(b=>new Float32Array(b)),fetch(`/api/animations/${a.id}/rotations.f32`).then(r=>r.arrayBuffer()).then(b=>new Float32Array(b))]);frame=0;playbackBlend=0;last=performance.now();snapCamera();slider.max=a.frames-1;renderGallery();draw()}function renderGallery(){if(!animations.length){items.textContent='No animations yet.';return}items.replaceChildren(...animations.map(a=>{const b=document.createElement('button');b.className='item '+a.status+(selected?.id===a.id?' active':'');b.disabled=a.status!=='ready';b.innerHTML=`<p>${a.prompt}</p><span class="status">${a.status} · ${a.frames} frames · ${a.diffusion_steps} steps</span>${a.error?`<div class="error">${a.error}</div>`:''}`;b.onclick=()=>select(a);return b}))}function showProgress(){const a=animations.find(a=>a.id===activeRequest);if(!a)return;if(a.status==='ready'){status.className='readout';status.textContent='Generation complete — select it from the gallery to play it.';activeRequest=undefined;generate.disabled=false;return}if(a.status==='failed'){status.className='error';status.textContent=`Generation failed: ${a.error}`;activeRequest=undefined;generate.disabled=false;return}const seconds=Math.max(0,Math.floor((Date.now()-activeStarted)/1000));status.className='readout progress';status.textContent=a.status==='running'?`Generating motion… ${seconds}s elapsed`:`Queued for generation… ${seconds}s elapsed`;generate.disabled=true}async function refresh(){animations=await fetch('/api/animations').then(r=>r.json());renderGallery();showProgress()}generate.onclick=async()=>{const prompt=promptBox.value.trim();if(!prompt)return;generate.disabled=true;status.className='readout progress';status.textContent='Submitting generation…';try{const r=await fetch('/api/generate',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({prompt,frames:150,steps:100,seed:0})});if(!r.ok)throw new Error(await r.text());const a=await r.json();activeRequest=a.id;activeStarted=Date.now();await refresh()}catch(e){status.className='error';status.textContent=e.message;generate.disabled=false}}; async function select(a){if(a.status!=='ready')return;selected=a;download.href=`/api/animations/${a.id}/animation.glb`;download.download=`kimodo-${a.id}.glb`;download.hidden=false;promptBox.value=a.prompt;window.dispatchEvent(new CustomEvent('kimodo:restore-sequence',{detail:{segments:a.segments,model:a.model}}));status.className='readout';status.textContent=`Selected ${a.id.slice(0,8)} · sequence restored`;[root,rotations]=await Promise.all([fetch(`/api/animations/${a.id}/root.f32`).then(r=>r.arrayBuffer()).then(b=>new Float32Array(b)),fetch(`/api/animations/${a.id}/rotations.f32`).then(r=>r.arrayBuffer()).then(b=>new Float32Array(b))]);frame=0;playbackBlend=0;last=performance.now();snapCamera();slider.max=a.frames-1;renderGallery();draw()}function renderGallery(){if(!animations.length){items.textContent='No animations yet.';return}items.replaceChildren(...animations.map(a=>{const b=document.createElement('button');b.className='item '+a.status+(selected?.id===a.id?' active':'');b.disabled=a.status!=='ready';b.innerHTML=`<p>${a.prompt}</p><span class="status">${a.status} · ${a.frames} frames · ${a.diffusion_steps} steps</span>${a.error?`<div class="error">${a.error}</div>`:''}`;b.onclick=()=>select(a);return b}))}function showProgress(){const a=animations.find(a=>a.id===activeRequest);if(!a)return;if(a.status==='ready'){status.className='readout';status.textContent='Generation complete — select it from the gallery to play it.';activeRequest=undefined;generate.disabled=false;return}if(a.status==='failed'){status.className='error';status.textContent=`Generation failed: ${a.error}`;activeRequest=undefined;generate.disabled=false;return}const seconds=Math.max(0,Math.floor((Date.now()-activeStarted)/1000));status.className='readout progress';status.textContent=a.status==='running'?`Generating motion… ${seconds}s elapsed`:`Queued for generation… ${seconds}s elapsed`;generate.disabled=true}async function refresh(){animations=await fetch('/api/animations').then(r=>r.json());renderGallery();showProgress()}generate.onclick=async()=>{const prompt=promptBox.value.trim();if(!prompt)return;generate.disabled=true;status.className='readout progress';status.textContent='Submitting generation…';try{const r=await fetch('/api/generate',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({prompt,frames:150,steps:100,seed:0})});if(!r.ok)throw new Error(await r.text());const a=await r.json();activeRequest=a.id;activeStarted=Date.now();await refresh()}catch(e){status.className='error';status.textContent=e.message;generate.disabled=false}};
document.querySelector('#play').onclick=e=>{playing=!playing;last=performance.now();playbackBlend=0;e.target.textContent=playing?'Pause':'Play'};slider.oninput=()=>{frame=Number(slider.value);playbackBlend=0;last=performance.now();draw()};document.querySelector('#reset').onclick=reset;canvas.addEventListener('dblclick',reset);canvas.addEventListener('contextmenu',e=>e.preventDefault());let drag;canvas.addEventListener('pointerdown',e=>{canvas.setPointerCapture(e.pointerId);drag={x:e.clientX,y:e.clientY,pan:e.button===2||e.shiftKey};canvas.classList.add('dragging')});canvas.addEventListener('pointermove',e=>{if(!drag)return;const dx=e.clientX-drag.x,dy=e.clientY-drag.y;drag.x=e.clientX;e.clientY;drag.y=e.clientY;if(drag.pan){view.panX+=dx;view.panY+=dy}else{view.yaw+=dx*.008;view.pitch=Math.max(-1.25,Math.min(1.25,view.pitch+dy*.008))}draw()});function end(){drag=undefined;canvas.classList.remove('dragging')}canvas.addEventListener('pointerup',end);canvas.addEventListener('pointercancel',end);canvas.addEventListener('wheel',e=>{e.preventDefault();view.zoom=Math.max(350,Math.min(3600,view.zoom*Math.exp(-e.deltaY*.001)));draw()},{passive:false});refresh().then(()=>{const a=animations.find(a=>a.status==='ready');if(a)return select(a);draw()});setInterval(refresh,2500);requestAnimationFrame(tick); document.querySelector('#play').onclick=e=>{playing=!playing;last=performance.now();playbackBlend=0;e.target.textContent=playing?'Pause':'Play'};slider.oninput=()=>{frame=Number(slider.value);playbackBlend=0;last=performance.now();draw()};document.querySelector('#reset').onclick=reset;canvas.addEventListener('dblclick',reset);canvas.addEventListener('contextmenu',e=>e.preventDefault());let drag;canvas.addEventListener('pointerdown',e=>{canvas.setPointerCapture(e.pointerId);drag={x:e.clientX,y:e.clientY,pan:e.button===2||e.shiftKey};canvas.classList.add('dragging')});canvas.addEventListener('pointermove',e=>{if(!drag)return;const dx=e.clientX-drag.x,dy=e.clientY-drag.y;drag.x=e.clientX;e.clientY;drag.y=e.clientY;if(drag.pan){view.panX+=dx;view.panY+=dy}else{view.yaw+=dx*.008;view.pitch=Math.max(-1.25,Math.min(1.25,view.pitch+dy*.008))}draw()});function end(){drag=undefined;canvas.classList.remove('dragging')}canvas.addEventListener('pointerup',end);canvas.addEventListener('pointercancel',end);canvas.addEventListener('wheel',e=>{e.preventDefault();view.zoom=Math.max(350,Math.min(3600,view.zoom*Math.exp(-e.deltaY*.001)));draw()},{passive:false});refresh().then(()=>{const a=animations.find(a=>a.status==='ready');if(a)return select(a);draw()});setInterval(refresh,2500);requestAnimationFrame(tick);
</script></html> </script></html>

View File

@ -54,9 +54,15 @@ type motionModel struct {
ID string `json:"id"` ID string `json:"id"`
Label string `json:"label"` Label string `json:"label"`
Skeleton string `json:"skeleton"` Skeleton string `json:"skeleton"`
SkeletonKey string `json:"skeleton_key"`
Upstream string `json:"upstream"` Upstream string `json:"upstream"`
License string `json:"license"`
LicenseURL string `json:"license_url"`
Commercial bool `json:"commercial"`
Available bool `json:"available"` Available bool `json:"available"`
Reason string `json:"reason,omitempty"` Reason string `json:"reason,omitempty"`
Parents []int `json:"parents"`
Offsets [][3]float32 `json:"offsets"`
Motion string `json:"-"` Motion string `json:"-"`
} }
type gallery struct { type gallery struct {
@ -94,14 +100,6 @@ func (g *gallery) list() []*animation {
return result return result
} }
func copyFile(dst, src string) error {
b, err := os.ReadFile(src)
if err != nil {
return err
}
return os.WriteFile(dst, b, 0600)
}
func readF32(path string) ([]float32, error) { func readF32(path string) ([]float32, error) {
b, err := os.ReadFile(path) b, err := os.ReadFile(path)
if err != nil { if err != nil {
@ -125,79 +123,159 @@ func writeF32(path string, values []float32) error {
return os.WriteFile(path, b, 0600) return os.WriteFile(path, b, 0600)
} }
func blendQuaternion(a, b []float32, alpha float32) { // Each motion is exported as a node-only GLB: it deliberately has no mesh or
dot := a[0]*b[0] + a[1]*b[1] + a[2]*b[2] + a[3]*b[3] // skin, so consumers can attach their own Three.js geometry to the named
if dot < 0 { // joints. Kimodo stores root translations and local XYZW rotations.
for i := range b { var smplx22Parents = [...]int{-1, 0, 0, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 9, 9, 12, 13, 14, 16, 17, 18, 19}
b[i] = -b[i] var smplx22Names = [...]string{"pelvis", "left_hip", "right_hip", "spine1", "left_knee", "right_knee", "spine2", "left_ankle", "right_ankle", "spine3", "left_foot", "right_foot", "neck", "left_collar", "right_collar", "head", "left_shoulder", "right_shoulder", "left_elbow", "right_elbow", "left_wrist", "right_wrist"}
} var smplx22Offsets = [...][3]float32{{}, {.052299, -.093936, -.027607}, {-.057193, -.106548, -.022218}, {-.001496, .11293, -.024981}, {.058867, -.416442, -.006557}, {-.048074, -.39756, -.014061}, {.0069, .145636, -.006859}, {-.041738, -.437584, -.029512}, {.014489, -.446853, -.01803}, {-.010334, .056082, .021116}, {.049294, -.065279, .126259}, {-.040575, -.065287, .127076}, {-.011026, .171365, -.028827}, {.047725, .087643, -.008375}, {-.046636, .086612, -.014864}, {.024654, .175391, .024463}, {.126285, .05768, -.013885}, {-.109342, .053674, -.009118}, {.272907, -.069853, -.039094}, {-.292029, -.03544, -.024565}, {.276174, .021254, -.002478}, {-.271878, -.004835, -.016445}}
}
length := float32(0) type skeletonDefinition struct {
for i := range a { key string
a[i] = alpha*a[i] + (1-alpha)*b[i] names []string
length += a[i] * a[i] parents []int
} offsets [][3]float32
if length > 0 {
length = 1 / float32(math.Sqrt(float64(length)))
for i := range a {
a[i] *= length
}
}
} }
// stitchSegments joins independently sampled demo segments. The overlap is var skeletonDefinitions = map[string]skeletonDefinition{
// blended in root space and by normalized linear interpolation for quaternions. "smplx22": {key: "smplx22", names: smplx22Names[:], parents: smplx22Parents[:], offsets: smplx22Offsets[:]},
// Native observed-motion conditioning is deliberately a later parity step. }
func stitchSegments(output string, dirs []string, overlap int) error {
var roots, rotations []float32 type gltfBufferView struct {
for index, dir := range dirs { Buffer int `json:"buffer"`
root, err := readF32(filepath.Join(dir, "root_positions.f32")) ByteOffset int `json:"byteOffset,omitempty"`
ByteLength int `json:"byteLength"`
}
type gltfAccessor struct {
BufferView int `json:"bufferView"`
ComponentType int `json:"componentType"`
Count int `json:"count"`
Type string `json:"type"`
}
func appendF32(dst []byte, values []float32) []byte {
for _, value := range values {
var b [4]byte
binary.LittleEndian.PutUint32(b[:], math.Float32bits(value))
dst = append(dst, b[:]...)
}
return dst
}
func writeSkeletonGLB(path string, roots, rotations []float32, skeleton skeletonDefinition) error {
frames := len(roots) / 3
joints := len(skeleton.parents)
if frames < 1 || joints < 1 || len(skeleton.names) != joints || len(skeleton.offsets) != joints || len(roots) != frames*3 || len(rotations) != frames*joints*4 {
return fmt.Errorf("invalid %s motion for GLB export", skeleton.key)
}
times := make([]float32, frames)
for i := range times {
times[i] = float32(i) / 30
}
bin := make([]byte, 0, (frames+frames*3+frames*22*4)*4)
views := make([]gltfBufferView, 0, 24)
addView := func(values []float32) int {
offset := len(bin)
bin = appendF32(bin, values)
views = append(views, gltfBufferView{Buffer: 0, ByteOffset: offset, ByteLength: len(bin) - offset})
return len(views) - 1
}
timeView, rootView := addView(times), addView(roots)
rotationViews := make([]int, joints)
for joint := range rotationViews {
track := make([]float32, frames*4)
for frame := 0; frame < frames; frame++ {
copy(track[frame*4:], rotations[(frame*joints+joint)*4:(frame*joints+joint+1)*4])
}
rotationViews[joint] = addView(track)
}
accessors := []gltfAccessor{{BufferView: timeView, ComponentType: 5126, Count: frames, Type: "SCALAR"}, {BufferView: rootView, ComponentType: 5126, Count: frames, Type: "VEC3"}}
for _, view := range rotationViews {
accessors = append(accessors, gltfAccessor{BufferView: view, ComponentType: 5126, Count: frames, Type: "VEC4"})
}
nodes := make([]map[string]any, joints)
for joint := range nodes {
node := map[string]any{"name": skeleton.names[joint]}
if joint != 0 {
node["translation"] = skeleton.offsets[joint]
}
children := make([]int, 0, 3)
for child, parent := range skeleton.parents {
if parent == joint {
children = append(children, child)
}
}
if len(children) != 0 {
node["children"] = children
}
nodes[joint] = node
}
samplers := make([]map[string]any, 0, 23)
channels := make([]map[string]any, 0, 23)
addChannel := func(node, output int, path string) {
samplers = append(samplers, map[string]any{"input": 0, "output": output, "interpolation": "LINEAR"})
channels = append(channels, map[string]any{"sampler": len(samplers) - 1, "target": map[string]any{"node": node, "path": path}})
}
addChannel(0, 1, "translation")
for joint := 0; joint < joints; joint++ {
addChannel(joint, joint+2, "rotation")
}
document := map[string]any{
"asset": map[string]string{"version": "2.0", "generator": "kimodo.cpp skeleton exporter"},
"scene": 0,
"scenes": []map[string]any{{"nodes": []int{0}}},
"nodes": nodes,
"buffers": []map[string]int{{"byteLength": len(bin)}},
"bufferViews": views,
"accessors": accessors,
"animations": []map[string]any{{"name": "KimodoMotion", "samplers": samplers, "channels": channels}},
"extras": map[string]any{"skeleton": skeleton.key, "fps": 30, "rotation_order": "xyzw"},
}
jsonChunk, err := json.Marshal(document)
if err != nil { if err != nil {
return err return err
} }
rot, err := readF32(filepath.Join(dir, "local_rotations_xyzw.f32")) for len(jsonChunk)%4 != 0 {
jsonChunk = append(jsonChunk, ' ')
}
for len(bin)%4 != 0 {
bin = append(bin, 0)
}
total := 12 + 8 + len(jsonChunk) + 8 + len(bin)
out := make([]byte, 0, total)
putU32 := func(value uint32) {
var b [4]byte
binary.LittleEndian.PutUint32(b[:], value)
out = append(out, b[:]...)
}
putU32(0x46546c67)
putU32(2)
putU32(uint32(total))
putU32(uint32(len(jsonChunk)))
putU32(0x4e4f534a)
out = append(out, jsonChunk...)
putU32(uint32(len(bin)))
putU32(0x004e4942)
out = append(out, bin...)
return os.WriteFile(path, out, 0600)
}
func exportSkeletonGLB(dir, skeletonKey string) error {
skeleton, ok := skeletonDefinitions[skeletonKey]
if !ok {
return fmt.Errorf("unsupported skeleton %q", skeletonKey)
}
roots, err := readF32(filepath.Join(dir, "root_positions.f32"))
if err != nil { if err != nil {
return err return err
} }
frames := len(root) / 3 rotations, err := readF32(filepath.Join(dir, "local_rotations_xyzw.f32"))
if frames == 0 || len(rot) != frames*22*4 { if err != nil {
return fmt.Errorf("invalid motion segment %d", index+1)
}
if index == 0 {
roots, rotations = root, rot
continue
}
n := overlap
if n > frames {
n = frames
}
if n > len(roots)/3 {
n = len(roots) / 3
}
for frame := 0; frame < n; frame++ {
alpha := float32(0.5)
if n > 1 {
alpha = 1 - float32(frame)/float32(n-1)
}
old := (len(roots)/3 - n + frame) * 3
newest := frame * 3
for axis := 0; axis < 3; axis++ {
roots[old+axis] = alpha*roots[old+axis] + (1-alpha)*root[newest+axis]
}
for joint := 0; joint < 22; joint++ {
oldQ := (len(rotations)/4 - n*22 + frame*22 + joint) * 4
newQ := (frame*22 + joint) * 4
blendQuaternion(rotations[oldQ:oldQ+4], append([]float32(nil), rot[newQ:newQ+4]...), alpha)
}
}
roots = append(roots, root[n*3:]...)
rotations = append(rotations, rot[n*22*4:]...)
}
if err := writeF32(filepath.Join(output, "root_positions.f32"), roots); err != nil {
return err return err
} }
return writeF32(filepath.Join(output, "local_rotations_xyzw.f32"), rotations) return writeSkeletonGLB(filepath.Join(dir, "animation.glb"), roots, rotations, skeleton)
} }
func (g *gallery) worker() { func (g *gallery) worker() {
for id := range g.queue { for id := range g.queue {
g.mu.Lock() g.mu.Lock()
@ -239,6 +317,9 @@ func (g *gallery) worker() {
err = fmt.Errorf("sequence: %w: %s", runErr, strings.TrimSpace(string(output))) err = fmt.Errorf("sequence: %w: %s", runErr, strings.TrimSpace(string(output)))
} }
} }
if err == nil {
err = exportSkeletonGLB(dir, model.SkeletonKey)
}
} }
} }
g.mu.Lock() g.mu.Lock()
@ -259,6 +340,10 @@ func (g *gallery) worker() {
func main() { func main() {
addr := flag.String("addr", "127.0.0.1:8090", "listen address") addr := flag.String("addr", "127.0.0.1:8090", "listen address")
motion := flag.String("motion-model", "models/kimodo-smplx-rp-v1-f32.gguf", "motion GGUF") motion := flag.String("motion-model", "models/kimodo-smplx-rp-v1-f32.gguf", "motion GGUF")
somaRP := flag.String("soma-rp-model", "models/kimodo-soma-rp-v1.1-f32.gguf", "SOMA RP v1.1 motion GGUF")
somaSEED := flag.String("soma-seed-model", "models/kimodo-soma-seed-v1.1-f32.gguf", "SOMA SEED v1.1 motion GGUF")
g1RP := flag.String("g1-rp-model", "models/kimodo-g1-rp-v1-f32.gguf", "G1 RP v1 motion GGUF")
g1SEED := flag.String("g1-seed-model", "models/kimodo-g1-seed-v1-f32.gguf", "G1 SEED v1 motion GGUF")
text := flag.String("text-bundle", "generated/llm2vec-text-bundle", "native LLM2Vec component directory") text := flag.String("text-bundle", "generated/llm2vec-text-bundle", "native LLM2Vec component directory")
generator := flag.String("generator", "build/debug/kmd-generate", "native text-to-motion command") generator := flag.String("generator", "build/debug/kmd-generate", "native text-to-motion command")
output := flag.String("output", "demo-output", "persistent gallery directory") output := flag.String("output", "demo-output", "persistent gallery directory")
@ -266,12 +351,24 @@ func main() {
if err := os.MkdirAll(*output, 0755); err != nil { if err := os.MkdirAll(*output, 0755); err != nil {
log.Fatal(err) log.Fatal(err)
} }
makeModel := func(id, label, skeletonLabel, skeletonKey, upstream, license, licenseURL, path string, commercial bool) motionModel {
definition := skeletonDefinitions[skeletonKey]
model := motionModel{ID: id, Label: label, Skeleton: skeletonLabel, SkeletonKey: skeletonKey, Upstream: upstream, License: license, LicenseURL: licenseURL, Commercial: commercial, Parents: definition.parents, Offsets: definition.offsets, Motion: path}
if info, err := os.Stat(path); err == nil && info.Mode().IsRegular() {
model.Available = true
} else {
model.Reason = "GGUF not found at " + path
}
return model
}
const internalLicense = "https://www.nvidia.com/en-us/agreements/enterprise-software/nvidia-internal-scientific-research-and-development-model-license/"
const openLicense = "https://www.nvidia.com/en-us/agreements/enterprise-software/nvidia-open-model-license/"
models := map[string]motionModel{ models := map[string]motionModel{
"smplx-rp-v1": {ID: "smplx-rp-v1", Label: "SMPL-X RP v1", Skeleton: "SMPL-X 22 joints", Upstream: "nvidia/Kimodo-SMPLX-RP-v1", Available: true, Motion: *motion}, "smplx-rp-v1": makeModel("smplx-rp-v1", "SMPL-X RP v1", "SMPL-X 22 joints", "smplx22", "nvidia/Kimodo-SMPLX-RP-v1", "NVIDIA Internal Scientific R&D (non-commercial)", internalLicense, *motion, false),
"soma-rp-v1.1": {ID: "soma-rp-v1.1", Label: "SOMA RP v1.1", Skeleton: "SOMA 30 joints", Upstream: "nvidia/Kimodo-SOMA-RP-v1.1", Reason: "SOMA decoder and GGML conversion are being added"}, "soma-rp-v1.1": makeModel("soma-rp-v1.1", "SOMA RP v1.1", "SOMA compact 30-joint control skeleton", "soma30", "nvidia/Kimodo-SOMA-RP-v1.1", "NVIDIA Open Model License", openLicense, *somaRP, true),
"soma-seed-v1.1": {ID: "soma-seed-v1.1", Label: "SOMA SEED v1.1", Skeleton: "SOMA 30 joints", Upstream: "nvidia/Kimodo-SOMA-SEED-v1.1", Reason: "SOMA decoder and GGML conversion are being added"}, "soma-seed-v1.1": makeModel("soma-seed-v1.1", "SOMA SEED v1.1", "SOMA compact 30-joint control skeleton", "soma30", "nvidia/Kimodo-SOMA-SEED-v1.1", "NVIDIA Open Model License", openLicense, *somaSEED, true),
"g1-rp-v1": {ID: "g1-rp-v1", Label: "G1 RP v1", Skeleton: "Unitree G1 34 joints", Upstream: "nvidia/Kimodo-G1-RP-v1", Reason: "G1 decoder and GGML conversion are being added"}, "g1-rp-v1": makeModel("g1-rp-v1", "G1 RP v1", "Unitree G1 34 joints", "g1skel34", "nvidia/Kimodo-G1-RP-v1", "NVIDIA Open Model License", openLicense, *g1RP, true),
"g1-seed-v1": {ID: "g1-seed-v1", Label: "G1 SEED v1", Skeleton: "Unitree G1 34 joints", Upstream: "nvidia/Kimodo-G1-SEED-v1", Reason: "G1 decoder and GGML conversion are being added"}, "g1-seed-v1": makeModel("g1-seed-v1", "G1 SEED v1", "Unitree G1 34 joints", "g1skel34", "nvidia/Kimodo-G1-SEED-v1", "NVIDIA Open Model License", openLicense, *g1SEED, true),
} }
g := &gallery{items: map[string]*animation{}, output: *output, queue: make(chan string, 32), generator: *generator, motion: *motion, text: *text, models: models} g := &gallery{items: map[string]*animation{}, output: *output, queue: make(chan string, 32), generator: *generator, motion: *motion, text: *text, models: models}
entries, _ := filepath.Glob(filepath.Join(*output, "*.json")) entries, _ := filepath.Glob(filepath.Join(*output, "*.json"))
@ -283,6 +380,15 @@ func main() {
var a animation var a animation
if json.Unmarshal(b, &a) == nil { if json.Unmarshal(b, &a) == nil {
g.items[a.ID] = &a g.items[a.ID] = &a
if a.Status == "ready" {
model, ok := models[a.Model]
if !ok {
model = models["smplx-rp-v1"]
}
if err := exportSkeletonGLB(filepath.Join(*output, a.ID), model.SkeletonKey); err != nil && !os.IsNotExist(err) {
log.Printf("export existing animation %s: %v", a.ID, err)
}
}
} }
} }
go g.worker() go g.worker()
@ -400,7 +506,7 @@ func main() {
}) })
mux.HandleFunc("/api/animations/", func(w http.ResponseWriter, r *http.Request) { mux.HandleFunc("/api/animations/", func(w http.ResponseWriter, r *http.Request) {
parts := strings.Split(strings.TrimPrefix(r.URL.Path, "/api/animations/"), "/") parts := strings.Split(strings.TrimPrefix(r.URL.Path, "/api/animations/"), "/")
if len(parts) != 2 || (parts[1] != "root.f32" && parts[1] != "rotations.f32") { if len(parts) != 2 || (parts[1] != "root.f32" && parts[1] != "rotations.f32" && parts[1] != "animation.glb") {
http.NotFound(w, r) http.NotFound(w, r)
return return
} }
@ -415,7 +521,23 @@ func main() {
if parts[1] == "rotations.f32" { if parts[1] == "rotations.f32" {
name = "local_rotations_xyzw.f32" name = "local_rotations_xyzw.f32"
} }
if parts[1] == "animation.glb" {
name = "animation.glb"
w.Header().Set("Content-Type", "model/gltf-binary")
w.Header().Set("Content-Disposition", "attachment; filename=kimodo-"+a.ID+".glb")
// A GLB is a compact asset; read it directly so browsers always receive
// it as a download rather than invoking any path-cleaning redirects.
data, err := os.ReadFile(filepath.Join(g.output, a.ID, name))
if err != nil {
http.NotFound(w, r)
return
}
w.Header().Set("Content-Length", fmt.Sprint(len(data)))
_, _ = w.Write(data)
return
} else {
w.Header().Set("Content-Type", "application/octet-stream") w.Header().Set("Content-Type", "application/octet-stream")
}
w.Header().Set("Cache-Control", "no-store") w.Header().Set("Cache-Control", "no-store")
http.ServeFile(w, r, filepath.Join(g.output, a.ID, name)) http.ServeFile(w, r, filepath.Join(g.output, a.ID, name))
}) })

55
demo/main_test.go Normal file
View File

@ -0,0 +1,55 @@
package main
import (
"encoding/binary"
"encoding/json"
"os"
"path/filepath"
"testing"
)
func TestWriteSkeletonGLB(t *testing.T) {
for _, skeletonKey := range []string{"smplx22", "soma30", "g1skel34"} {
t.Run(skeletonKey, func(t *testing.T) {
skeleton := skeletonDefinitions[skeletonKey]
path := filepath.Join(t.TempDir(), "animation.glb")
roots := []float32{0, 0, 0, 1, 0, 0}
rotations := make([]float32, 2*len(skeleton.parents)*4)
for frame := 0; frame < 2; frame++ {
for joint := range skeleton.parents {
rotations[(frame*len(skeleton.parents)+joint)*4+3] = 1
}
}
if err := writeSkeletonGLB(path, roots, rotations, skeleton); err != nil {
t.Fatal(err)
}
assertSkeletonGLB(t, path, len(skeleton.parents))
})
}
}
func assertSkeletonGLB(t *testing.T, path string, expectedJoints int) {
t.Helper()
b, err := os.ReadFile(path)
if err != nil {
t.Fatal(err)
}
if len(b) < 20 || binary.LittleEndian.Uint32(b) != 0x46546c67 || binary.LittleEndian.Uint32(b[4:]) != 2 || int(binary.LittleEndian.Uint32(b[8:])) != len(b) {
t.Fatalf("invalid GLB header")
}
jsonLength := int(binary.LittleEndian.Uint32(b[12:]))
if binary.LittleEndian.Uint32(b[16:]) != 0x4e4f534a || 20+jsonLength > len(b) {
t.Fatalf("invalid GLB JSON chunk")
}
var document struct {
Asset map[string]string `json:"asset"`
Nodes []json.RawMessage `json:"nodes"`
Animations []json.RawMessage `json:"animations"`
}
if err := json.Unmarshal(b[20:20+jsonLength], &document); err != nil {
t.Fatal(err)
}
if document.Asset["version"] != "2.0" || len(document.Nodes) != expectedJoints || len(document.Animations) != 1 {
t.Fatalf("unexpected GLB document: %s", b[20:20+jsonLength])
}
}

View File

@ -5,6 +5,7 @@ window.addEventListener('load', async () => {
const generate = document.querySelector('#generate'); const generate = document.querySelector('#generate');
if (!prompt || !form || !generate) return; if (!prompt || !form || !generate) return;
const models = await fetch('/api/models').then(r => r.json()); const models = await fetch('/api/models').then(r => r.json());
window.kimodoModels = models;
const modelLabel = document.createElement('label'); const modelLabel = document.createElement('label');
modelLabel.htmlFor = 'motionModel'; modelLabel.textContent = 'Motion model'; modelLabel.htmlFor = 'motionModel'; modelLabel.textContent = 'Motion model';
@ -19,7 +20,14 @@ window.addEventListener('load', async () => {
const modelHint = document.createElement('div'); modelHint.className = 'hint'; const modelHint = document.createElement('div'); modelHint.className = 'hint';
const updateModel = () => { const updateModel = () => {
const model = models.find(item => item.id === select.value); const model = models.find(item => item.id === select.value);
modelHint.textContent = model.available ? `${model.skeleton} · ${model.upstream}` : `${model.skeleton} · ${model.reason}`; if (!model) return;
const title = document.querySelector('.eyebrow');
if (title) title.textContent = `${model.label} · Vulkan`;
modelHint.classList.toggle('license-warning', !model.commercial);
const terms = model.commercial ? 'commercial use permitted under NVIDIA Open Model License' : '⚠ non-commercial research use only';
const detail = model.available ? `${model.skeleton} · ${model.upstream} · ` : `${model.skeleton} · ${model.reason} · `;
const link = document.createElement('a'); link.href = model.license_url; link.target = '_blank'; link.rel = 'noreferrer'; link.textContent = terms;
modelHint.replaceChildren(document.createTextNode(detail), link);
}; };
select.onchange = updateModel; select.onchange = updateModel;
form.insertBefore(modelLabel, prompt); form.insertBefore(select, prompt); form.insertBefore(modelHint, prompt); updateModel(); form.insertBefore(modelLabel, prompt); form.insertBefore(select, prompt); form.insertBefore(modelHint, prompt); updateModel();

19
demo/skeletons_extra.go Normal file
View File

@ -0,0 +1,19 @@
package main
// Copied from NVIDIA Kimodo's Apache-2.0 skeleton definitions. Offsets are
// the parent-local differences extracted from the official neutral-joint
// assets in the trusted reference container.
func init() {
skeletonDefinitions["soma30"] = skeletonDefinition{
key: "soma30",
names: []string{"Hips", "Spine1", "Spine2", "Chest", "Neck1", "Neck2", "Head", "Jaw", "LeftEye", "RightEye", "LeftShoulder", "LeftArm", "LeftForeArm", "LeftHand", "LeftHandThumbEnd", "LeftHandMiddleEnd", "RightShoulder", "RightArm", "RightForeArm", "RightHand", "RightHandThumbEnd", "RightHandMiddleEnd", "LeftLeg", "LeftShin", "LeftFoot", "LeftToeBase", "RightLeg", "RightShin", "RightFoot", "RightToeBase"},
parents: []int{-1, 0, 1, 2, 3, 4, 5, 6, 6, 6, 3, 10, 11, 12, 13, 13, 3, 16, 17, 18, 19, 19, 0, 22, 23, 24, 0, 26, 27, 28},
offsets: [][3]float32{{0, 0, 0}, {-.00013727, .0500376256, -.00053726669}, {-1.86574103e-9, .0712530139, -.000298248546}, {-5.75188398e-9, .0755006305, -.00815970992}, {-.00181676517, .263112953, -.00553348292}, {-2.85102231e-8, .0770939664, .0230258546}, {-4.5975437e-8, .0612891595, .0195370861}, {2.63687901e-5, .0047559225, .0309494062}, {.0320638079, .0538020513, .0758688308}, {-.0322244017, .05361869, .0755823359}, {.0162165175, .232371641, .0511341324}, {.149198457, 2.19397873e-8, -.0550232576}, {.287393078, 2.50268389e-9, -2.58787737e-5}, {.270939812, -7.06625108e-9, 2.60897248e-5}, {.122686267, -.0322017573, .0483306876}, {.190119595, -.00312878387, -.000339570373}, {-.0138011824, .231803086, .0521415786}, {-.150371962, 1.17387901e-7, -.0554560437}, {-.287366393, 1.87628082e-8, -2.59709359e-5}, {-.271336198, -1.16767401e-9, 2.61269368e-5}, {-.122642483, -.0321145448, .0480403904}, {-.190005945, -.00306615542, -.0003157343}, {.10043214, -.0843452671, .0259565473}, {-1e-8, -.432217537, -.00802912805}, {1e-8, -.421550959, -.0348152298}, {0, -.0505947206, .132315294}, {-.10047278, -.0829525995, .0262031695}, {1e-8, -.433622059, -.00805555828}, {2e-8, -.421173943, -.0347839785}, {-3.42907669e-9, -.0507960932, .132841956}},
}
skeletonDefinitions["g1skel34"] = skeletonDefinition{
key: "g1skel34",
names: []string{"pelvis_skel", "left_hip_pitch_skel", "left_hip_roll_skel", "left_hip_yaw_skel", "left_knee_skel", "left_ankle_pitch_skel", "left_ankle_roll_skel", "left_toe_base", "right_hip_pitch_skel", "right_hip_roll_skel", "right_hip_yaw_skel", "right_knee_skel", "right_ankle_pitch_skel", "right_ankle_roll_skel", "right_toe_base", "waist_yaw_skel", "waist_roll_skel", "waist_pitch_skel", "left_shoulder_pitch_skel", "left_shoulder_roll_skel", "left_shoulder_yaw_skel", "left_elbow_skel", "left_wrist_roll_skel", "left_wrist_pitch_skel", "left_wrist_yaw_skel", "left_hand_roll_skel", "right_shoulder_pitch_skel", "right_shoulder_roll_skel", "right_shoulder_yaw_skel", "right_elbow_skel", "right_wrist_roll_skel", "right_wrist_pitch_skel", "right_wrist_yaw_skel", "right_hand_roll_skel"},
parents: []int{-1, 0, 1, 2, 3, 4, 5, 6, 0, 8, 9, 10, 11, 12, 13, 0, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 17, 26, 27, 28, 29, 30, 31, 32},
offsets: [][3]float32{{0, 0, 0}, {.064452, -.1027, 0}, {.052, -.030465, 0}, {0, -.12412, .025001}, {.0021489, -.17734, -.078273}, {-.000094445, -.30001, 0}, {0, -.017558, 0}, {0, -.035, .14}, {-.064452, -.1027, 0}, {-.052, -.030465, 0}, {0, -.12412, .025001}, {-.0021489, -.17734, -.078273}, {.000094445, -.30001, 0}, {0, -.017558, 0}, {0, -.035, .14}, {0, 0, 0}, {0, .044, -.0039635}, {0, 0, 0}, {.10022, .24778, .0039563}, {.038, -.013831, 0}, {.00624, -.1032, 0}, {0, -.080518, .015783}, {.00188791, -.01, .1}, {0, 0, .038}, {0, 0, .046}, {0, 0, .1}, {-.10021, .24778, .0039563}, {-.038, -.013831, 0}, {-.00624, -.1032, 0}, {0, -.080518, .015783}, {-.00188791, -.01, .1}, {0, 0, .038}, {0, 0, .046}, {0, 0, .1}},
}
}

View File

@ -32,7 +32,7 @@ def parse_args() -> argparse.Namespace:
parser.add_argument("--seed", required=True, type=int) parser.add_argument("--seed", required=True, type=int)
parser.add_argument("--device", default="cuda") parser.add_argument("--device", default="cuda")
parser.add_argument("--checkpoint-dir", type=Path, parser.add_argument("--checkpoint-dir", type=Path,
help="local directory containing Kimodo-SMPLX-RP-v1; never download at capture time") help="local directory containing the selected Kimodo checkpoint; never download at capture time")
parser.add_argument("--zero-embedding", action="store_true", parser.add_argument("--zero-embedding", action="store_true",
help="use a deterministic [1,1,4096] zero embedding; enables motion-only fixtures") help="use a deterministic [1,1,4096] zero embedding; enables motion-only fixtures")
parser.add_argument("--text-base", type=Path, parser.add_argument("--text-base", type=Path,
@ -113,8 +113,21 @@ def main() -> None:
raise SystemExit("--zero-embedding cannot be combined with real text model paths") raise SystemExit("--zero-embedding cannot be combined with real text model paths")
if args.checkpoint_dir: if args.checkpoint_dir:
checkpoint = args.checkpoint_dir.resolve() checkpoint = args.checkpoint_dir.resolve()
if not (checkpoint / "Kimodo-SMPLX-RP-v1" / "config.yaml").is_file(): model_folders = {
raise SystemExit("--checkpoint-dir must contain Kimodo-SMPLX-RP-v1/config.yaml") "kimodo-smplx-rp": "Kimodo-SMPLX-RP-v1",
"kimodo-smplx-rp-v1": "Kimodo-SMPLX-RP-v1",
"kimodo-soma-rp": "Kimodo-SOMA-RP-v1.1",
"kimodo-soma-rp-v1.1": "Kimodo-SOMA-RP-v1.1",
"kimodo-soma-seed": "Kimodo-SOMA-SEED-v1.1",
"kimodo-soma-seed-v1.1": "Kimodo-SOMA-SEED-v1.1",
"kimodo-g1-rp": "Kimodo-G1-RP-v1",
"kimodo-g1-rp-v1": "Kimodo-G1-RP-v1",
"kimodo-g1-seed": "Kimodo-G1-SEED-v1",
"kimodo-g1-seed-v1": "Kimodo-G1-SEED-v1",
}
folder = model_folders.get(args.model)
if folder is None or not (checkpoint / folder / "config.yaml").is_file():
raise SystemExit("--checkpoint-dir does not contain the selected official Kimodo model")
# This is deliberately set only for the reference subprocess. It # This is deliberately set only for the reference subprocess. It
# prevents a missing local model from silently falling back to HF. # prevents a missing local model from silently falling back to HF.
import os import os

View File

@ -1,5 +1,5 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
"""Convert the Kimodo SMPL-X safetensors checkpoint to a self-describing GGUF. """Convert a supported Kimodo motion safetensors checkpoint to GGUF.
This converter deliberately implements only the safe safetensors and NPY This converter deliberately implements only the safe safetensors and NPY
formats. It never imports torch, never deserializes pickle, and writes to a formats. It never imports torch, never deserializes pickle, and writes to a
@ -22,6 +22,15 @@ GGUF_MAGIC, GGUF_VERSION, GGML_TYPE_F32 = 0x46554747, 3, 0
TYPE_UINT64, TYPE_STRING, TYPE_FLOAT32 = 10, 8, 6 TYPE_UINT64, TYPE_STRING, TYPE_FLOAT32 = 10, 8, 6
TYPE_UINT32 = 4 TYPE_UINT32 = 4
MODEL_SPECS = {
"nvidia/Kimodo-SMPLX-RP-v1": ("smplx22", "SMPLXSkeleton22", 22, False,
"nvidia-internal-scientific-research-and-development-model-license"),
"nvidia/Kimodo-SOMA-RP-v1.1": ("soma30", "SOMASkeleton30", 30, True, "nvidia-open-model-license"),
"nvidia/Kimodo-SOMA-SEED-v1.1": ("soma30", "SOMASkeleton30", 30, True, "nvidia-open-model-license"),
"nvidia/Kimodo-G1-RP-v1": ("g1skel34", "G1Skeleton34", 34, True, "nvidia-open-model-license"),
"nvidia/Kimodo-G1-SEED-v1": ("g1skel34", "G1Skeleton34", 34, True, "nvidia-open-model-license"),
}
@dataclass(frozen=True) @dataclass(frozen=True)
class Tensor: class Tensor:
name: str name: str
@ -183,7 +192,7 @@ def copy_range(dst, tensor: Tensor) -> None:
def main() -> None: def main() -> None:
p = argparse.ArgumentParser(description=__doc__) p = argparse.ArgumentParser(description=__doc__)
p.add_argument("--input", required=True, type=Path, help="downloaded Kimodo-SMPLX-RP-v1 directory") p.add_argument("--input", required=True, type=Path, help="downloaded Kimodo model directory")
p.add_argument("--output", required=True, type=Path) p.add_argument("--output", required=True, type=Path)
args = p.parse_args() args = p.parse_args()
root = args.input.resolve() root = args.input.resolve()
@ -195,22 +204,33 @@ def main() -> None:
for part in ("global_root", "local_root", "body"): for part in ("global_root", "local_root", "body"):
for stat in ("mean", "std"): for stat in ("mean", "std"):
tensors.append(read_npy(root / "stats" / "motion" / part / f"{stat}.npy", f"stats.{part}.{stat}")) tensors.append(read_npy(root / "stats" / "motion" / part / f"{stat}.npy", f"stats.{part}.{stat}"))
revision = (root / "REVISION").read_text(encoding="utf-8").split()[0] revision_fields = (root / "REVISION").read_text(encoding="utf-8").split()
if len(revision_fields) != 2 or revision_fields[1] not in MODEL_SPECS:
raise SystemExit("REVISION does not identify a supported official Kimodo model")
revision, model_id = revision_fields
skeleton, skeleton_class, joints, commercial, license_name = MODEL_SPECS[model_id]
config = (root / "config.yaml").read_text(encoding="utf-8")
if f"_target_: kimodo.skeleton.{skeleton_class}" not in config:
raise SystemExit("config.yaml skeleton does not match REVISION model identity")
motion_dim = 9 + 12 * joints
body_dim = motion_dim - 5
meta = [ meta = [
metadata_string("general.architecture", "kimodo-motion"), metadata_string("general.architecture", "kimodo-motion"),
metadata_string("general.name", "Kimodo-SMPLX-RP-v1"), metadata_string("general.name", model_id.removeprefix("nvidia/")),
# GGML's own loader requires general.alignment to be UINT32. # GGML's own loader requires general.alignment to be UINT32.
metadata_uint32("general.alignment", ALIGNMENT), metadata_uint32("general.alignment", ALIGNMENT),
metadata_uint("kimodo.format_version", 1), metadata_uint("kimodo.format_version", 1),
metadata_string("kimodo.skeleton", "smplx22"), metadata_string("kimodo.skeleton", skeleton),
metadata_string("kimodo.model_identity", f"nvidia/Kimodo-SMPLX-RP-v1@{revision}"), metadata_string("kimodo.model_identity", f"{model_id}@{revision}"),
metadata_string("kimodo.license", license_name),
metadata_uint("kimodo.commercial_use", int(commercial)),
metadata_string("kimodo.source_revision", revision), metadata_string("kimodo.source_revision", revision),
metadata_string("kimodo.source_sha256", sha256(ckpt)), metadata_string("kimodo.source_sha256", sha256(ckpt)),
metadata_uint("kimodo.text_embedding_width", 4096), metadata_uint("kimodo.text_embedding_width", 4096),
metadata_uint("kimodo.motion_dim", 273), metadata_uint("kimodo.motion_dim", motion_dim),
metadata_uint("kimodo.global_root_dim", 5), metadata_uint("kimodo.global_root_dim", 5),
metadata_uint("kimodo.local_root_dim", 4), metadata_uint("kimodo.local_root_dim", 4),
metadata_uint("kimodo.body_dim", 268), metadata_uint("kimodo.body_dim", body_dim),
metadata_uint("kimodo.hidden_size", 1024), metadata_uint("kimodo.hidden_size", 1024),
metadata_uint("kimodo.layers", 16), metadata_uint("kimodo.layers", 16),
metadata_uint("kimodo.heads", 8), metadata_uint("kimodo.heads", 8),

View File

@ -4,19 +4,20 @@ set -euo pipefail
export HF_HUB_DISABLE_PROGRESS_BARS=1 export HF_HUB_DISABLE_PROGRESS_BARS=1
ORG="${GGUF_ORG:-LocalAI-io}" ORG="${GGUF_ORG:-LocalAI-io}"
MOTION_REPO_DEFAULT="$ORG/Kimodo-SMPLX-RP-v1-GGML"
TEXT_REPO_DEFAULT="$ORG/Llama-3-Kimodo-GGML" TEXT_REPO_DEFAULT="$ORG/Llama-3-Kimodo-GGML"
usage() { usage() {
printf '%s\n' "usage: $0 --output DIR [--motion-repo HF_REPO] [--text-repo HF_REPO] [--revision REVISION] [--motion-only]" >&2 printf '%s\n' "usage: $0 --output DIR [--model MODEL]... [--motion-repo HF_REPO] [--text-repo HF_REPO] [--revision REVISION] [--motion-only]" >&2
printf '%s\n' "models: soma-rp-v1.1, soma-seed-v1.1, g1-rp-v1, g1-seed-v1" >&2
exit 2 exit 2
} }
output='' motion_repo="$MOTION_REPO_DEFAULT" text_repo="$TEXT_REPO_DEFAULT" revision='main' motion_only=0 output='' motion_repo_override='' text_repo="$TEXT_REPO_DEFAULT" revision='main' motion_only=0 models=()
while [ "$#" -gt 0 ]; do while [ "$#" -gt 0 ]; do
case "$1" in case "$1" in
--output) [ "$#" -ge 2 ] || usage; output=$2; shift 2 ;; --output) [ "$#" -ge 2 ] || usage; output=$2; shift 2 ;;
--motion-repo) [ "$#" -ge 2 ] || usage; motion_repo=$2; shift 2 ;; --model) [ "$#" -ge 2 ] || usage; models+=("$2"); shift 2 ;;
--motion-repo) [ "$#" -ge 2 ] || usage; motion_repo_override=$2; shift 2 ;;
--text-repo) [ "$#" -ge 2 ] || usage; text_repo=$2; shift 2 ;; --text-repo) [ "$#" -ge 2 ] || usage; text_repo=$2; shift 2 ;;
--revision) [ "$#" -ge 2 ] || usage; revision=$2; shift 2 ;; --revision) [ "$#" -ge 2 ] || usage; revision=$2; shift 2 ;;
--motion-only) motion_only=1; shift ;; --motion-only) motion_only=1; shift ;;
@ -25,6 +26,8 @@ while [ "$#" -gt 0 ]; do
done done
[ -n "$output" ] || usage [ -n "$output" ] || usage
command -v hf >/dev/null || { echo "hf not found; enter the Nix shell first" >&2; exit 1; } command -v hf >/dev/null || { echo "hf not found; enter the Nix shell first" >&2; exit 1; }
[ "${#models[@]}" -gt 0 ] || models=(soma-rp-v1.1)
[ -z "$motion_repo_override" ] || [ "${#models[@]}" -eq 1 ] || { echo "--motion-repo requires exactly one --model" >&2; exit 2; }
mkdir -p "$output" mkdir -p "$output"
@ -70,7 +73,17 @@ print("verified native Kimodo GGUF bundle")
PY PY
} }
download_and_verify "$motion_repo" "models/kimodo-smplx-rp-v1-f32.gguf" for model in "${models[@]}"; do
case "$model" in
soma-rp-v1.1) motion_repo="$ORG/Kimodo-SOMA-RP-v1.1-GGML"; motion_file='models/kimodo-soma-rp-v1.1-f32.gguf' ;;
soma-seed-v1.1) motion_repo="$ORG/Kimodo-SOMA-SEED-v1.1-GGML"; motion_file='models/kimodo-soma-seed-v1.1-f32.gguf' ;;
g1-rp-v1) motion_repo="$ORG/Kimodo-G1-RP-v1-GGML"; motion_file='models/kimodo-g1-rp-v1-f32.gguf' ;;
g1-seed-v1) motion_repo="$ORG/Kimodo-G1-SEED-v1-GGML"; motion_file='models/kimodo-g1-seed-v1-f32.gguf' ;;
*) echo "unknown motion model: $model" >&2; usage ;;
esac
[ -z "$motion_repo_override" ] || motion_repo=$motion_repo_override
download_and_verify "$motion_repo" "$motion_file"
done
if [ "$motion_only" -eq 0 ]; then if [ "$motion_only" -eq 0 ]; then
download_and_verify "$text_repo" "generated/llm2vec-text-bundle/*" download_and_verify "$text_repo" "generated/llm2vec-text-bundle/*"
fi fi

View File

@ -6,15 +6,17 @@ set -euo pipefail
export HF_HUB_DISABLE_PROGRESS_BARS=1 export HF_HUB_DISABLE_PROGRESS_BARS=1
usage() { usage() {
printf '%s\n' "usage: $0 --output DIR [--revision REVISION] [--with-text]" >&2 printf '%s\n' "usage: $0 --output DIR [--revision REVISION] [--model NAME]... [--with-text]" >&2
exit 2 exit 2
} }
output='' revision='main' with_text=0 output='' revision='main' with_text=0
models=()
while [ "$#" -gt 0 ]; do while [ "$#" -gt 0 ]; do
case "$1" in case "$1" in
--output) [ "$#" -ge 2 ] || usage; output=$2; shift 2 ;; --output) [ "$#" -ge 2 ] || usage; output=$2; shift 2 ;;
--revision) [ "$#" -ge 2 ] || usage; revision=$2; shift 2 ;; --revision) [ "$#" -ge 2 ] || usage; revision=$2; shift 2 ;;
--model) [ "$#" -ge 2 ] || usage; models+=("$2"); shift 2 ;;
--with-text) with_text=1; shift ;; --with-text) with_text=1; shift ;;
*) usage ;; *) usage ;;
esac esac
@ -52,7 +54,18 @@ download() {
printf '%s %s\n' "$sha" "$repo" > "$target/REVISION" printf '%s %s\n' "$sha" "$repo" > "$target/REVISION"
} }
download nvidia/Kimodo-SMPLX-RP-v1 Kimodo-SMPLX-RP-v1 if [ "${#models[@]}" -eq 0 ]; then models=(smplx-rp-v1); fi
for model in "${models[@]}"; do
case "$model" in
smplx-rp-v1) repo=nvidia/Kimodo-SMPLX-RP-v1; folder=Kimodo-SMPLX-RP-v1 ;;
soma-rp-v1.1) repo=nvidia/Kimodo-SOMA-RP-v1.1; folder=Kimodo-SOMA-RP-v1.1 ;;
soma-seed-v1.1) repo=nvidia/Kimodo-SOMA-SEED-v1.1; folder=Kimodo-SOMA-SEED-v1.1 ;;
g1-rp-v1) repo=nvidia/Kimodo-G1-RP-v1; folder=Kimodo-G1-RP-v1 ;;
g1-seed-v1) repo=nvidia/Kimodo-G1-SEED-v1; folder=Kimodo-G1-SEED-v1 ;;
*) echo "Unknown Kimodo model: $model" >&2; usage ;;
esac
download "$repo" "$folder"
done
if [ "$with_text" -eq 1 ]; then if [ "$with_text" -eq 1 ]; then
# The MNTP repo is a LoRA adapter, not the Llama base checkpoint. Keep all # The MNTP repo is a LoRA adapter, not the Llama base checkpoint. Keep all
# three identities separately so converter provenance cannot confuse them. # three identities separately so converter provenance cannot confuse them.

View File

@ -0,0 +1,4 @@
Licensed by NVIDIA Corporation under the NVIDIA Open Model License.
Upstream model: nvidia/Kimodo-G1-RP-v1. This GGUF conversion grants no
additional rights.

View File

@ -0,0 +1,25 @@
---
license: other
library_name: ggml
base_model: nvidia/Kimodo-G1-RP-v1
base_model_relation: quantized
tags: [gguf, ggml, text-to-motion, unitree-g1, kimodo]
---
# Kimodo-G1-RP-v1-GGML
Native F32 GGML/GGUF conversion of
[nvidia/Kimodo-G1-RP-v1](https://huggingface.co/nvidia/Kimodo-G1-RP-v1),
targeting the 34-joint Unitree G1 skeleton. Its reusable Llama-derived text
encoder is distributed separately as
[`Llama-3-Kimodo-GGML`](https://huggingface.co/LocalAI-io/Llama-3-Kimodo-GGML).
The model is installed at `models/kimodo-g1-rp-v1-f32.gguf` by
`scripts/download_gguf_weights.sh --output "$PWD" --model g1-rp-v1`.
## Provenance and licence
Converted from upstream revision `3020ad8c419c244e0429d360163730c63c4ed011`.
`MANIFEST.json` records the source revision and SHA-256 of the GGUF. The model
remains subject to the [NVIDIA Open Model License](https://www.nvidia.com/en-us/agreements/enterprise-software/nvidia-open-model-license/).
This conversion grants no additional rights.

View File

@ -0,0 +1,4 @@
Licensed by NVIDIA Corporation under the NVIDIA Open Model License.
Upstream model: nvidia/Kimodo-G1-SEED-v1. This GGUF conversion grants no
additional rights.

View File

@ -0,0 +1,25 @@
---
license: other
library_name: ggml
base_model: nvidia/Kimodo-G1-SEED-v1
base_model_relation: quantized
tags: [gguf, ggml, text-to-motion, unitree-g1, kimodo]
---
# Kimodo-G1-SEED-v1-GGML
Native F32 GGML/GGUF conversion of
[nvidia/Kimodo-G1-SEED-v1](https://huggingface.co/nvidia/Kimodo-G1-SEED-v1),
targeting the 34-joint Unitree G1 skeleton. Its reusable Llama-derived text
encoder is distributed separately as
[`Llama-3-Kimodo-GGML`](https://huggingface.co/LocalAI-io/Llama-3-Kimodo-GGML).
The model is installed at `models/kimodo-g1-seed-v1-f32.gguf` by
`scripts/download_gguf_weights.sh --output "$PWD" --model g1-seed-v1`.
## Provenance and licence
Converted from upstream revision `5e6f2c7e18c2ab834c8d7983b9dcce701e5c6097`.
`MANIFEST.json` records the source revision and SHA-256 of the GGUF. The model
remains subject to the [NVIDIA Open Model License](https://www.nvidia.com/en-us/agreements/enterprise-software/nvidia-open-model-license/).
This conversion grants no additional rights.

View File

@ -1,3 +1,4 @@
Kimodo-SMPLX-RP-v1 source model: NVIDIA. This converted distribution remains Kimodo-SMPLX-RP-v1 source model: NVIDIA. The NVIDIA Internal Scientific
subject to the NVIDIA Internal Scientific Research and Development Model License Research and Development Model License limits derivative models to internal,
and is for non-commercial research use only. non-production R&D and prohibits their distribution. This notice is retained
for local conversion records; it does not authorize a converted distribution.

View File

@ -6,30 +6,31 @@ base_model_relation: quantized
tags: [gguf, ggml, text-to-motion, smplx, kimodo] tags: [gguf, ggml, text-to-motion, smplx, kimodo]
--- ---
# Kimodo-SMPLX-RP-v1-GGML # Oops — Kimodo-SMPLX-RP-v1 is local-conversion only
Native F32 GGML/GGUF conversion of The kimodo.cpp converter can produce a local F32 GGML/GGUF representation of
[nvidia/Kimodo-SMPLX-RP-v1](https://huggingface.co/nvidia/Kimodo-SMPLX-RP-v1), [nvidia/Kimodo-SMPLX-RP-v1](https://huggingface.co/nvidia/Kimodo-SMPLX-RP-v1),
the SMPL-X 22-joint text-and-constraint conditioned motion diffusion model. the SMPL-X 22-joint text-and-constraint conditioned motion diffusion model.
This repository contains only the diffusion model; its reusable Llama-derived It is not a redistributable GGUF release.
text encoder is distributed separately as
[`Llama-3-Kimodo-GGML`](https://huggingface.co/LocalAI-io/Llama-3-Kimodo-GGML).
From a kimodo.cpp checkout with the Hugging Face CLI installed, install both with: We originally published the converted weights here, then noticed that the
upstream NVIDIA Internal Scientific Research and Development Model License
explicitly prohibits distributing derivative models. Oops. The GGUF, manifest,
and checksums have therefore been removed; this card remains so that existing
links explain what happened instead of becoming a mysterious 404.
```sh If someone at NVIDIA is willing to give LocalAI-io written permission to
scripts/download_gguf_weights.sh --output "$PWD" redistribute this checkpoint as GGML/GGUF, that would be very welcome. We would
``` be happy to restore the conversion with its upstream provenance and verified
parity results.
The model is installed at `models/kimodo-smplx-rp-v1-f32.gguf`. Use
`--motion-only` when supplying a precomputed LLM2Vec embedding.
## Provenance and licence ## Provenance and licence
Converted by kimodo.cpp from upstream commit The local converter targets upstream revision
`1419ba56b734c48bbafb41fefa84088ca94583b5`. `MANIFEST.json` records the `1419ba56b734c48bbafb41fefa84088ca94583b5`.
source revision and SHA-256 of the GGUF.
Kimodo-SMPLX-RP-v1 is for non-commercial research use only and remains subject Kimodo-SMPLX-RP-v1 remains subject to the
to the [NVIDIA Internal Scientific Research and Development Model License](https://huggingface.co/nvidia/Kimodo-SMPLX-RP-v1). [NVIDIA Internal Scientific Research and Development Model License](https://www.nvidia.com/en-us/agreements/enterprise-software/nvidia-internal-scientific-research-and-development-model-license/).
This conversion grants no additional rights. That licence limits the checkpoint and derivative models to internal,
non-production R&D and prohibits their distribution. Converting the weights to
GGUF does not grant additional rights.

View File

@ -0,0 +1,4 @@
Licensed by NVIDIA Corporation under the NVIDIA Open Model License.
Upstream model: nvidia/Kimodo-SOMA-RP-v1.1. This GGUF conversion grants no
additional rights.

View File

@ -0,0 +1,25 @@
---
license: other
library_name: ggml
base_model: nvidia/Kimodo-SOMA-RP-v1.1
base_model_relation: quantized
tags: [gguf, ggml, text-to-motion, soma, kimodo]
---
# Kimodo-SOMA-RP-v1.1-GGML
Native F32 GGML/GGUF conversion of
[nvidia/Kimodo-SOMA-RP-v1.1](https://huggingface.co/nvidia/Kimodo-SOMA-RP-v1.1).
The model predicts the compact SOMA 30-joint control skeleton. Its reusable
Llama-derived text encoder is distributed separately as
[`Llama-3-Kimodo-GGML`](https://huggingface.co/LocalAI-io/Llama-3-Kimodo-GGML).
The model is installed at `models/kimodo-soma-rp-v1.1-f32.gguf` by
`scripts/download_gguf_weights.sh --output "$PWD" --model soma-rp-v1.1`.
## Provenance and licence
Converted from upstream revision `6c9233af1180b8151e3c4703477104af5dce9dd5`.
`MANIFEST.json` records the source revision and SHA-256 of the GGUF. The model
remains subject to the [NVIDIA Open Model License](https://www.nvidia.com/en-us/agreements/enterprise-software/nvidia-open-model-license/).
This conversion grants no additional rights.

View File

@ -0,0 +1,4 @@
Licensed by NVIDIA Corporation under the NVIDIA Open Model License.
Upstream model: nvidia/Kimodo-SOMA-SEED-v1.1. This GGUF conversion grants no
additional rights.

View File

@ -0,0 +1,25 @@
---
license: other
library_name: ggml
base_model: nvidia/Kimodo-SOMA-SEED-v1.1
base_model_relation: quantized
tags: [gguf, ggml, text-to-motion, soma, kimodo]
---
# Kimodo-SOMA-SEED-v1.1-GGML
Native F32 GGML/GGUF conversion of
[nvidia/Kimodo-SOMA-SEED-v1.1](https://huggingface.co/nvidia/Kimodo-SOMA-SEED-v1.1).
The model predicts the compact SOMA 30-joint control skeleton. Its reusable
Llama-derived text encoder is distributed separately as
[`Llama-3-Kimodo-GGML`](https://huggingface.co/LocalAI-io/Llama-3-Kimodo-GGML).
The model is installed at `models/kimodo-soma-seed-v1.1-f32.gguf` by
`scripts/download_gguf_weights.sh --output "$PWD" --model soma-seed-v1.1`.
## Provenance and licence
Converted from upstream revision `aae3af194322c60d21bc44062b64c3fec912be50`.
`MANIFEST.json` records the source revision and SHA-256 of the GGUF. The model
remains subject to the [NVIDIA Open Model License](https://www.nvidia.com/en-us/agreements/enterprise-software/nvidia-open-model-license/).
This conversion grants no additional rights.

View File

@ -23,15 +23,54 @@ ROOT = Path(__file__).resolve().parent.parent
HF_ORG = "LocalAI-io" # Hugging Face organisation; GitHub is localai-org. HF_ORG = "LocalAI-io" # Hugging Face organisation; GitHub is localai-org.
DEFAULT_REPOS = { DEFAULT_REPOS = {
"text": f"{HF_ORG}/Llama-3-Kimodo-GGML", "text": f"{HF_ORG}/Llama-3-Kimodo-GGML",
"motion": f"{HF_ORG}/Kimodo-SMPLX-RP-v1-GGML",
} }
MOTION_NAME = "kimodo-smplx-rp-v1-f32.gguf" MOTION_MODELS = {
"smplx-rp-v1": {
"repo": f"{HF_ORG}/Kimodo-SMPLX-RP-v1-GGML",
"source": "nvidia/Kimodo-SMPLX-RP-v1",
"folder": "Kimodo-SMPLX-RP-v1",
"revision": "1419ba56b734c48bbafb41fefa84088ca94583b5",
"file": "kimodo-smplx-rp-v1-f32.gguf",
"redistributable": False,
},
"soma-rp-v1.1": {
"repo": f"{HF_ORG}/Kimodo-SOMA-RP-v1.1-GGML",
"source": "nvidia/Kimodo-SOMA-RP-v1.1",
"folder": "Kimodo-SOMA-RP-v1.1",
"revision": "6c9233af1180b8151e3c4703477104af5dce9dd5",
"file": "kimodo-soma-rp-v1.1-f32.gguf",
"redistributable": True,
},
"soma-seed-v1.1": {
"repo": f"{HF_ORG}/Kimodo-SOMA-SEED-v1.1-GGML",
"source": "nvidia/Kimodo-SOMA-SEED-v1.1",
"folder": "Kimodo-SOMA-SEED-v1.1",
"revision": "aae3af194322c60d21bc44062b64c3fec912be50",
"file": "kimodo-soma-seed-v1.1-f32.gguf",
"redistributable": True,
},
"g1-rp-v1": {
"repo": f"{HF_ORG}/Kimodo-G1-RP-v1-GGML",
"source": "nvidia/Kimodo-G1-RP-v1",
"folder": "Kimodo-G1-RP-v1",
"revision": "3020ad8c419c244e0429d360163730c63c4ed011",
"file": "kimodo-g1-rp-v1-f32.gguf",
"redistributable": True,
},
"g1-seed-v1": {
"repo": f"{HF_ORG}/Kimodo-G1-SEED-v1-GGML",
"source": "nvidia/Kimodo-G1-SEED-v1",
"folder": "Kimodo-G1-SEED-v1",
"revision": "5e6f2c7e18c2ab834c8d7983b9dcce701e5c6097",
"file": "kimodo-g1-seed-v1-f32.gguf",
"redistributable": True,
},
}
TEXT_NAMES = ( TEXT_NAMES = (
"tokenizer.gguf", "embedding.gguf", "final-norm.gguf", "tokenizer.gguf", "embedding.gguf", "final-norm.gguf",
*(f"layer-{index:02d}.gguf" for index in range(32)), *(f"layer-{index:02d}.gguf" for index in range(32)),
) )
SOURCE_REVISIONS = { SOURCE_REVISIONS = {
"nvidia/Kimodo-SMPLX-RP-v1": "1419ba56b734c48bbafb41fefa84088ca94583b5",
"meta-llama/Meta-Llama-3-8B-Instruct": "8afb486c1db24fe5011ec46dfbe5b5dccdb575c2", "meta-llama/Meta-Llama-3-8B-Instruct": "8afb486c1db24fe5011ec46dfbe5b5dccdb575c2",
"McGill-NLP/LLM2Vec-Meta-Llama-3-8B-Instruct-mntp": "31474e395ada192e8ed1586db6be79fb3b70c9c0", "McGill-NLP/LLM2Vec-Meta-Llama-3-8B-Instruct-mntp": "31474e395ada192e8ed1586db6be79fb3b70c9c0",
"McGill-NLP/LLM2Vec-Meta-Llama-3-8B-Instruct-mntp-supervised": "baa8ebf04a1c2500e61288e7dad65e8ae42601a7", "McGill-NLP/LLM2Vec-Meta-Llama-3-8B-Instruct-mntp-supervised": "baa8ebf04a1c2500e61288e7dad65e8ae42601a7",
@ -47,24 +86,23 @@ def digest(path: Path) -> str:
return value.hexdigest() return value.hexdigest()
def require_revision(repo: str) -> None: def require_revision(repo: str, expected: str, folder: str | None = None) -> None:
revision = ROOT / "models" / { revision = ROOT / "models" / (folder or {
"nvidia/Kimodo-SMPLX-RP-v1": "Kimodo-SMPLX-RP-v1",
"meta-llama/Meta-Llama-3-8B-Instruct": "llama3-8b-instruct-base", "meta-llama/Meta-Llama-3-8B-Instruct": "llama3-8b-instruct-base",
"McGill-NLP/LLM2Vec-Meta-Llama-3-8B-Instruct-mntp": "llm2vec-mntp-adapter", "McGill-NLP/LLM2Vec-Meta-Llama-3-8B-Instruct-mntp": "llm2vec-mntp-adapter",
"McGill-NLP/LLM2Vec-Meta-Llama-3-8B-Instruct-mntp-supervised": "llm2vec-adapter", "McGill-NLP/LLM2Vec-Meta-Llama-3-8B-Instruct-mntp-supervised": "llm2vec-adapter",
}[repo] / "REVISION" }[repo]) / "REVISION"
if not revision.is_file(): if not revision.is_file():
raise ValueError(f"missing provenance file: {revision}") raise ValueError(f"missing provenance file: {revision}")
actual = revision.read_text(encoding="utf-8").split()[0] actual = revision.read_text(encoding="utf-8").split()[0]
if actual != SOURCE_REVISIONS[repo]: if actual != expected:
raise ValueError(f"unexpected {repo} revision: {actual} (expected {SOURCE_REVISIONS[repo]})") raise ValueError(f"unexpected {repo} revision: {actual} (expected {expected})")
def artifacts(component: str, motion: Path, bundle: Path) -> list[tuple[Path, str]]: def artifacts(component: str, motion: Path, motion_name: str, bundle: Path) -> list[tuple[Path, str]]:
result: list[tuple[Path, str]] = [] result: list[tuple[Path, str]] = []
if component == "motion": if component == "motion":
result.append((motion, f"models/{MOTION_NAME}")) result.append((motion, f"models/{motion_name}"))
else: else:
result.extend((bundle / name, f"generated/llm2vec-text-bundle/{name}") for name in TEXT_NAMES) result.extend((bundle / name, f"generated/llm2vec-text-bundle/{name}") for name in TEXT_NAMES)
for source, destination in result: for source, destination in result:
@ -82,7 +120,9 @@ def artifacts(component: str, motion: Path, bundle: Path) -> list[tuple[Path, st
def main() -> int: def main() -> int:
parser = argparse.ArgumentParser(description=__doc__) parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--motion", type=Path, default=ROOT / "models" / MOTION_NAME) parser.add_argument("--motion", type=Path, default=None,
help="motion GGUF (defaults to the selected model's converted output)")
parser.add_argument("--motion-model", choices=tuple(MOTION_MODELS), default="soma-rp-v1.1")
parser.add_argument("--text-bundle", type=Path, parser.add_argument("--text-bundle", type=Path,
default=ROOT / "generated/llm2vec-text-bundle") default=ROOT / "generated/llm2vec-text-bundle")
parser.add_argument("--component", choices=("text", "motion"), required=True, parser.add_argument("--component", choices=("text", "motion"), required=True,
@ -93,21 +133,28 @@ def main() -> int:
parser.add_argument("--confirm-upstream-licences", action="store_true", parser.add_argument("--confirm-upstream-licences", action="store_true",
help="required with --upload; confirms authority to redistribute all inputs") help="required with --upload; confirms authority to redistribute all inputs")
args = parser.parse_args() args = parser.parse_args()
repo = args.repo or DEFAULT_REPOS[args.component] motion_spec = MOTION_MODELS[args.motion_model]
card_dir = ROOT / "scripts/hf" / ("Llama-3-Kimodo-GGML" if args.component == "text" else "Kimodo-SMPLX-RP-v1-GGML") if args.component == "motion" and args.upload and not motion_spec["redistributable"]:
print("error: the SMPL-X checkpoint licence prohibits distributing Derivative Models; local conversion only",
file=sys.stderr)
return 2
motion = args.motion or ROOT / "models" / motion_spec["file"]
repo = args.repo or (DEFAULT_REPOS["text"] if args.component == "text" else motion_spec["repo"])
card_dir = ROOT / "scripts/hf" / ("Llama-3-Kimodo-GGML" if args.component == "text" else repo.rsplit("/", 1)[1])
card = card_dir / "README.md" card = card_dir / "README.md"
notice = card_dir / "NOTICE" notice = card_dir / "NOTICE"
relevant_sources = (SOURCE_REVISIONS if args.component == "text" relevant_sources = (SOURCE_REVISIONS if args.component == "text"
else {"nvidia/Kimodo-SMPLX-RP-v1": SOURCE_REVISIONS["nvidia/Kimodo-SMPLX-RP-v1"]}) else {motion_spec["source"]: motion_spec["revision"]})
try: try:
if not card.is_file() or not notice.is_file(): if not card.is_file() or not notice.is_file():
raise ValueError("version-controlled model card or NOTICE is missing") raise ValueError("version-controlled model card or NOTICE is missing")
if args.component == "text" and not LLAMA_LICENSE.is_file(): if args.component == "text" and not LLAMA_LICENSE.is_file():
raise ValueError("Meta Llama 3 licence is missing") raise ValueError("Meta Llama 3 licence is missing")
for source_repo in relevant_sources: for source_repo, revision in relevant_sources.items():
require_revision(source_repo) folder = motion_spec["folder"] if args.component == "motion" else None
files = artifacts(args.component, args.motion, args.text_bundle) require_revision(source_repo, revision, folder)
files = artifacts(args.component, motion, motion_spec["file"], args.text_bundle)
except ValueError as error: except ValueError as error:
print(f"error: {error}", file=sys.stderr) print(f"error: {error}", file=sys.stderr)
return 1 return 1

View File

@ -90,9 +90,10 @@ std::expected<std::vector<float>, std::string> run_two_stage_denoiser(
std::span<const float> embedding, std::span<const float> timesteps, std::span<const float> embedding, std::span<const float> timesteps,
std::span<const float> headings, std::span<const float> mask, std::span<const float> headings, std::span<const float> mask,
std::size_t batch, std::size_t frames) { std::size_t batch, std::size_t frames) {
if (!batch || !frames || x.size()!=batch*frames*546 || mask.size()!=batch*frames) const size_t dim=weights.motion_dim(), root_input_dim=2*dim, body_input_dim=2*dim-1;
if (!batch || !frames || !dim || x.size()!=batch*frames*root_input_dim || mask.size()!=batch*frames)
return std::unexpected("invalid two-stage denoiser input dimensions"); return std::unexpected("invalid two-stage denoiser input dimensions");
auto root=run_motion_transformer(weights,"root_model.",x,546,embedding,timesteps,headings,batch,frames); auto root=run_motion_transformer(weights,"root_model.",x,root_input_dim,embedding,timesteps,headings,batch,frames);
if(!root)return std::unexpected(root.error()); if(!root)return std::unexpected(root.error());
auto gm=weights.f32_values("stats.global_root.mean"), gs=weights.f32_values("stats.global_root.std"), lm=weights.f32_values("stats.local_root.mean"), ls=weights.f32_values("stats.local_root.std"); auto gm=weights.f32_values("stats.global_root.mean"), gs=weights.f32_values("stats.global_root.std"), lm=weights.f32_values("stats.local_root.mean"), ls=weights.f32_values("stats.local_root.std");
if(!gm)return std::unexpected(gm.error()); if(!gm)return std::unexpected(gm.error());
@ -101,16 +102,16 @@ std::expected<std::vector<float>, std::string> run_two_stage_denoiser(
if(!ls)return std::unexpected(ls.error()); if(!ls)return std::unexpected(ls.error());
auto local=global_root_to_local_root(*root,mask,batch,frames,*gm,*gs,*lm,*ls); auto local=global_root_to_local_root(*root,mask,batch,frames,*gm,*gs,*lm,*ls);
if(!local)return std::unexpected(local.error()); if(!local)return std::unexpected(local.error());
std::vector<float> body_input(batch*frames*545); std::vector<float> body_input(batch*frames*body_input_dim);
for(std::size_t b=0;b<batch;++b) for(std::size_t t=0;t<frames;++t) { for(std::size_t b=0;b<batch;++b) for(std::size_t t=0;t<frames;++t) {
const auto src=(b*frames+t)*546, dst=(b*frames+t)*545; const auto src=(b*frames+t)*root_input_dim, dst=(b*frames+t)*body_input_dim;
std::memcpy(body_input.data()+dst,local->data()+(b*frames+t)*4,4*sizeof(float)); std::memcpy(body_input.data()+dst,local->data()+(b*frames+t)*4,4*sizeof(float));
std::memcpy(body_input.data()+dst+4,x.data()+src+5,541*sizeof(float)); std::memcpy(body_input.data()+dst+4,x.data()+src+5,(root_input_dim-5)*sizeof(float));
} }
auto body=run_motion_transformer(weights,"body_model.",body_input,545,embedding,timesteps,headings,batch,frames); auto body=run_motion_transformer(weights,"body_model.",body_input,body_input_dim,embedding,timesteps,headings,batch,frames);
if(!body)return std::unexpected(body.error()); if(!body)return std::unexpected(body.error());
std::vector<float> output(batch*frames*273); std::vector<float> output(batch*frames*dim);
for(std::size_t b=0;b<batch;++b)for(std::size_t t=0;t<frames;++t){const auto r=(b*frames+t)*5, q=(b*frames+t)*268, o=(b*frames+t)*273;std::memcpy(output.data()+o,root->data()+r,5*sizeof(float));std::memcpy(output.data()+o+5,body->data()+q,268*sizeof(float));} for(std::size_t b=0;b<batch;++b)for(std::size_t t=0;t<frames;++t){const auto r=(b*frames+t)*5, q=(b*frames+t)*(dim-5), o=(b*frames+t)*dim;std::memcpy(output.data()+o,root->data()+r,5*sizeof(float));std::memcpy(output.data()+o+5,body->data()+q,(dim-5)*sizeof(float));}
return output; return output;
} }
@ -118,7 +119,7 @@ std::expected<std::vector<float>, std::string> run_separated_cfg_denoiser(
const ggml_motion_weights &weights, std::span<const float> motion, const ggml_motion_weights &weights, std::span<const float> motion,
std::span<const float> embedding, float timestep, float text_weight, std::span<const float> embedding, float timestep, float text_weight,
float constraint_weight, std::size_t frames) { float constraint_weight, std::size_t frames) {
const std::vector<float> empty(frames*273, 0.f); const std::vector<float> empty(frames*weights.motion_dim(), 0.f);
return run_separated_cfg_denoiser_conditioned(weights, motion, embedding, empty, empty, return run_separated_cfg_denoiser_conditioned(weights, motion, embedding, empty, empty,
timestep, 0.f, text_weight, constraint_weight, frames); timestep, 0.f, text_weight, constraint_weight, frames);
} }
@ -128,25 +129,26 @@ std::expected<std::vector<float>, std::string> run_separated_cfg_denoiser_condit
std::span<const float> embedding, std::span<const float> observed, std::span<const float> embedding, std::span<const float> observed,
std::span<const float> observed_mask, float timestep, float heading, std::span<const float> observed_mask, float timestep, float heading,
float text_weight, float constraint_weight, std::size_t frames) { float text_weight, float constraint_weight, std::size_t frames) {
if (motion.size()!=frames*273 || embedding.size()!=4096 || !std::isfinite(timestep) || !std::isfinite(text_weight) || !std::isfinite(constraint_weight)) const size_t dim=weights.motion_dim();
if (!dim || motion.size()!=frames*dim || embedding.size()!=4096 || !std::isfinite(timestep) || !std::isfinite(text_weight) || !std::isfinite(constraint_weight))
return std::unexpected("invalid separated CFG denoiser input"); return std::unexpected("invalid separated CFG denoiser input");
if (observed.size()!=frames*273 || observed_mask.size()!=frames*273 || !std::isfinite(heading)) if (observed.size()!=frames*dim || observed_mask.size()!=frames*dim || !std::isfinite(heading))
return std::unexpected("invalid separated CFG condition dimensions"); return std::unexpected("invalid separated CFG condition dimensions");
constexpr size_t cfg_batch=3; std::vector<float> extended(cfg_batch*frames*546), text(cfg_batch*4096), times(cfg_batch,timestep), headings(cfg_batch,heading), mask(cfg_batch*frames,1.f); constexpr size_t cfg_batch=3; std::vector<float> extended(cfg_batch*frames*2*dim), text(cfg_batch*4096), times(cfg_batch,timestep), headings(cfg_batch,heading), mask(cfg_batch*frames,1.f);
for(size_t b=0;b<cfg_batch;++b) for(size_t t=0;t<frames;++t) { for(size_t b=0;b<cfg_batch;++b) for(size_t t=0;t<frames;++t) {
auto *dst=extended.data()+(b*frames+t)*546; auto *dst=extended.data()+(b*frames+t)*2*dim;
std::memcpy(dst,motion.data()+t*273,273*sizeof(float)); std::memcpy(dst,motion.data()+t*dim,dim*sizeof(float));
// Upstream separated CFG is [text, constraint, unconditional]. Only // Upstream separated CFG is [text, constraint, unconditional]. Only
// the constraint branch receives observed motion and its feature mask. // the constraint branch receives observed motion and its feature mask.
if (b==1) for (size_t d=0;d<273;++d) dst[d]=motion[t*273+d]*(1.f-observed_mask[t*273+d])+observed[t*273+d]*observed_mask[t*273+d]; if (b==1) for (size_t d=0;d<dim;++d) dst[d]=motion[t*dim+d]*(1.f-observed_mask[t*dim+d])+observed[t*dim+d]*observed_mask[t*dim+d];
if (b==1) std::memcpy(dst+273,observed_mask.data()+t*273,273*sizeof(float)); if (b==1) std::memcpy(dst+dim,observed_mask.data()+t*dim,dim*sizeof(float));
} }
// Only branch zero has text. Branch one is constraint-only; branch two is // Only branch zero has text. Branch one is constraint-only; branch two is
// unconditional. This is the upstream separated-CFG batch order. // unconditional. This is the upstream separated-CFG batch order.
std::memcpy(text.data(),embedding.data(),4096*sizeof(float)); std::memcpy(text.data(),embedding.data(),4096*sizeof(float));
auto all=run_two_stage_denoiser(weights,extended,text,times,headings,mask,cfg_batch,frames); auto all=run_two_stage_denoiser(weights,extended,text,times,headings,mask,cfg_batch,frames);
if(!all)return std::unexpected(all.error()); if(!all)return std::unexpected(all.error());
std::vector<float> result(frames*273); std::vector<float> result(frames*dim);
for(size_t i=0;i<result.size();++i) result[i]=(*all)[2*result.size()+i]+text_weight*((*all)[i]-(*all)[2*result.size()+i])+constraint_weight*((*all)[result.size()+i]-(*all)[2*result.size()+i]); for(size_t i=0;i<result.size();++i) result[i]=(*all)[2*result.size()+i]+text_weight*((*all)[i]-(*all)[2*result.size()+i])+constraint_weight*((*all)[result.size()+i]-(*all)[2*result.size()+i]);
return result; return result;
} }
@ -155,7 +157,7 @@ std::expected<std::vector<float>, std::string> sample_motion_from_noise(
const ggml_motion_weights &weights, std::span<const float> initial, const ggml_motion_weights &weights, std::span<const float> initial,
std::span<const float> embedding, std::size_t frames, unsigned steps, std::span<const float> embedding, std::size_t frames, unsigned steps,
float text_weight, float constraint_weight) { float text_weight, float constraint_weight) {
if(initial.size()!=frames*273) return std::unexpected("invalid initial motion noise dimensions"); if(initial.size()!=frames*weights.motion_dim()) return std::unexpected("invalid initial motion noise dimensions");
auto schedule=make_cosine_schedule(1000,steps); if(!schedule)return std::unexpected(schedule.error()); auto schedule=make_cosine_schedule(1000,steps); if(!schedule)return std::unexpected(schedule.error());
std::vector<float> state(initial.begin(),initial.end()), next(state.size()); std::vector<float> state(initial.begin(),initial.end()), next(state.size());
for(unsigned i=steps;i-->0;) { for(unsigned i=steps;i-->0;) {
@ -173,7 +175,7 @@ std::expected<std::vector<float>, std::string> sample_motion_from_noise_conditio
std::span<const float> embedding, std::span<const float> observed, std::span<const float> embedding, std::span<const float> observed,
std::span<const float> observed_mask, float heading, std::size_t frames, std::span<const float> observed_mask, float heading, std::size_t frames,
unsigned steps, float text_weight, float constraint_weight) { unsigned steps, float text_weight, float constraint_weight) {
if(initial.size()!=frames*273 || observed.size()!=initial.size() || observed_mask.size()!=initial.size()) if(initial.size()!=frames*weights.motion_dim() || observed.size()!=initial.size() || observed_mask.size()!=initial.size())
return std::unexpected("invalid conditioned motion noise dimensions"); return std::unexpected("invalid conditioned motion noise dimensions");
auto schedule=make_cosine_schedule(1000,steps); if(!schedule)return std::unexpected(schedule.error()); auto schedule=make_cosine_schedule(1000,steps); if(!schedule)return std::unexpected(schedule.error());
std::vector<float> state(initial.begin(),initial.end()), next(state.size()); std::vector<float> state(initial.begin(),initial.end()), next(state.size());

View File

@ -34,15 +34,15 @@ std::expected<std::vector<float>, std::string> run_motion_transformer(
std::span<const float> headings, std::size_t batch, std::size_t frames); std::span<const float> headings, std::size_t batch, std::size_t frames);
// Exact two-stage Kimodo denoiser for concatenated motion/mask inputs // Exact two-stage Kimodo denoiser for concatenated motion/mask inputs
// [B,T,546]. Returned clean prediction is [B,T,273]. // [B,T,2*motion_dim]. Returned clean prediction is [B,T,motion_dim].
std::expected<std::vector<float>, std::string> run_two_stage_denoiser( std::expected<std::vector<float>, std::string> run_two_stage_denoiser(
const ggml_motion_weights &weights, std::span<const float> motion_and_mask, const ggml_motion_weights &weights, std::span<const float> motion_and_mask,
std::span<const float> text_embedding, std::span<const float> timesteps, std::span<const float> text_embedding, std::span<const float> timesteps,
std::span<const float> headings, std::span<const float> motion_mask, std::span<const float> headings, std::span<const float> motion_mask,
std::size_t batch, std::size_t frames); std::size_t batch, std::size_t frames);
// Unconstrained separated CFG wrapper. `motion` is [T,273], embedding is // Unconstrained separated CFG wrapper. `motion` is [T,motion_dim], embedding
// [4096], and the result is one clean [T,273] prediction. // is [4096], and the result is one clean prediction.
std::expected<std::vector<float>, std::string> run_separated_cfg_denoiser( std::expected<std::vector<float>, std::string> run_separated_cfg_denoiser(
const ggml_motion_weights &weights, std::span<const float> motion, const ggml_motion_weights &weights, std::span<const float> motion,
std::span<const float> embedding, float timestep, float text_weight, std::span<const float> embedding, float timestep, float text_weight,
@ -54,7 +54,7 @@ std::expected<std::vector<float>, std::string> sample_motion_from_noise(
std::span<const float> embedding, std::size_t frames, unsigned steps, std::span<const float> embedding, std::size_t frames, unsigned steps,
float text_weight, float constraint_weight); float text_weight, float constraint_weight);
// Multi-prompt transition sampler. `observed` and `observed_mask` are [T,273] // Multi-prompt transition sampler. `observed` and `observed_mask` are [T,motion_dim]
// normalized motion-representation values/masks. This mirrors the upstream // normalized motion-representation values/masks. This mirrors the upstream
// concat-mask denoiser: text, constraint, and unconditional CFG branches. // concat-mask denoiser: text, constraint, and unconditional CFG branches.
std::expected<std::vector<float>, std::string> sample_motion_from_noise_conditioned( std::expected<std::vector<float>, std::string> sample_motion_from_noise_conditioned(

View File

@ -41,7 +41,7 @@ int main(int argc, char **argv) try {
const std::filesystem::path output(argv[7]); std::filesystem::create_directories(output); const std::filesystem::path output(argv[7]); std::filesystem::create_directories(output);
write_f32(output / "root_positions.f32", motion->root_positions); write_f32(output / "root_positions.f32", motion->root_positions);
write_f32(output / "local_rotations_xyzw.f32", motion->local_rotations_xyzw); write_f32(output / "local_rotations_xyzw.f32", motion->local_rotations_xyzw);
std::cout << "generated " << motion->frames << " SMPL-X22 sequence frames\n"; std::cout << "generated " << motion->frames << " frames with " << motion->joints << " joints\n";
return 0; return 0;
} }
if (argc != 8) { if (argc != 8) {
@ -63,7 +63,7 @@ int main(int argc, char **argv) try {
std::filesystem::create_directories(output); std::filesystem::create_directories(output);
write_f32(output / "root_positions.f32", motion->root_positions); write_f32(output / "root_positions.f32", motion->root_positions);
write_f32(output / "local_rotations_xyzw.f32", motion->local_rotations_xyzw); write_f32(output / "local_rotations_xyzw.f32", motion->local_rotations_xyzw);
std::cout << "generated " << motion->frames << " SMPL-X22 frames\n"; std::cout << "generated " << motion->frames << " frames with " << motion->joints << " joints\n";
return 0; return 0;
} catch (const std::exception &error) { } catch (const std::exception &error) {
std::cerr << error.what() << '\n'; std::cerr << error.what() << '\n';

View File

@ -61,6 +61,9 @@ std::expected<std::unique_ptr<ggml_motion_weights>, std::string> ggml_motion_wei
if (!checked) return std::unexpected(checked.error()); if (!checked) return std::unexpected(checked.error());
if (auto valid = validate_motion_gguf(*checked); !valid) return std::unexpected(valid.error()); if (auto valid = validate_motion_gguf(*checked); !valid) return std::unexpected(valid.error());
auto result = std::unique_ptr<ggml_motion_weights>(new ggml_motion_weights); auto result = std::unique_ptr<ggml_motion_weights>(new ggml_motion_weights);
result->skeleton_ = checked->strings.at("kimodo.skeleton");
result->motion_dim_ = static_cast<size_t>(checked->uints.at("kimodo.motion_dim"));
result->body_dim_ = static_cast<size_t>(checked->uints.at("kimodo.body_dim"));
gguf_init_params params{true, &result->context_}; gguf_init_params params{true, &result->context_};
result->gguf_ = gguf_init_from_file(std::string(path).c_str(), params); result->gguf_ = gguf_init_from_file(std::string(path).c_str(), params);
if (!result->gguf_ || !result->context_) return std::unexpected("GGML could not load checked motion GGUF"); if (!result->gguf_ || !result->context_) return std::unexpected("GGML could not load checked motion GGUF");

View File

@ -1,5 +1,6 @@
#pragma once #pragma once
#include <cstddef>
#include <expected> #include <expected>
#include <memory> #include <memory>
#include <string> #include <string>
@ -27,6 +28,9 @@ public:
ggml_tensor *tensor(std::string_view name) const; ggml_tensor *tensor(std::string_view name) const;
std::expected<std::vector<float>, std::string> f32_values(std::string_view name) const; std::expected<std::vector<float>, std::string> f32_values(std::string_view name) const;
ggml_backend *backend() const noexcept { return backend_; } ggml_backend *backend() const noexcept { return backend_; }
std::string_view skeleton_key() const noexcept { return skeleton_; }
std::size_t motion_dim() const noexcept { return motion_dim_; }
std::size_t body_dim() const noexcept { return body_dim_; }
private: private:
ggml_motion_weights() = default; ggml_motion_weights() = default;
@ -34,6 +38,9 @@ private:
gguf_context *gguf_ = nullptr; gguf_context *gguf_ = nullptr;
ggml_backend *backend_ = nullptr; ggml_backend *backend_ = nullptr;
ggml_backend_buffer *buffer_ = nullptr; ggml_backend_buffer *buffer_ = nullptr;
std::string skeleton_;
std::size_t motion_dim_ = 0;
std::size_t body_dim_ = 0;
}; };
} // namespace kimodo::detail } // namespace kimodo::detail

View File

@ -1,4 +1,5 @@
#include "gguf.hpp" #include "gguf.hpp"
#include "skeleton.hpp"
#include <algorithm> #include <algorithm>
#include <array> #include <array>
@ -106,8 +107,14 @@ std::expected<void, std::string> validate_motion_gguf(const gguf_file &file) {
if (format == file.uints.end() || format->second != 1) if (format == file.uints.end() || format->second != 1)
return std::unexpected("unsupported Kimodo motion GGUF format"); return std::unexpected("unsupported Kimodo motion GGUF format");
const auto skeleton = file.strings.find("kimodo.skeleton"); const auto skeleton = file.strings.find("kimodo.skeleton");
if (skeleton == file.strings.end() || skeleton->second != "smplx22") if (skeleton == file.strings.end() || !find_skeleton(skeleton->second))
return std::unexpected("first runtime supports only smplx22 skeletons"); return std::unexpected("motion GGUF has an unsupported skeleton");
const auto &spec = *find_skeleton(skeleton->second);
const auto motion_dim = file.uints.find("kimodo.motion_dim");
const auto body_dim = file.uints.find("kimodo.body_dim");
if (motion_dim == file.uints.end() || motion_dim->second != spec.motion_dim() ||
body_dim == file.uints.end() || body_dim->second != spec.body_dim())
return std::unexpected("motion GGUF dimensions do not match its skeleton");
const auto width = file.uints.find("kimodo.text_embedding_width"); const auto width = file.uints.find("kimodo.text_embedding_width");
if (width == file.uints.end() || width->second != 4096) if (width == file.uints.end() || width->second != 4096)
return std::unexpected("motion GGUF has incompatible text embedding width"); return std::unexpected("motion GGUF has incompatible text embedding width");

View File

@ -22,5 +22,5 @@ int main(int argc, char **argv) {
} }
gguf_free(file); ggml_free(tensor_context); gguf_free(file); ggml_free(tensor_context);
#endif #endif
std::puts("Kimodo SMPL-X motion GGUF: valid (414 F32 tensors)"); std::puts("Kimodo motion GGUF: valid (414 F32 tensors)");
} }

View File

@ -1,5 +1,6 @@
#include <kimodo/kimodo.hpp> #include <kimodo/kimodo.hpp>
#include "gguf.hpp" #include "gguf.hpp"
#include "skeleton.hpp"
#ifdef KIMODO_HAVE_GGML #ifdef KIMODO_HAVE_GGML
#include "ggml_weights.hpp" #include "ggml_weights.hpp"
#include "denoiser.hpp" #include "denoiser.hpp"
@ -15,6 +16,7 @@ namespace kimodo {
struct model::impl { struct model::impl {
detail::gguf_file motion; detail::gguf_file motion;
std::string motion_path; std::string motion_path;
const detail::skeleton_spec *skeleton = nullptr;
#ifdef KIMODO_HAVE_GGML #ifdef KIMODO_HAVE_GGML
mutable std::unique_ptr<detail::ggml_motion_weights> weights; mutable std::unique_ptr<detail::ggml_motion_weights> weights;
std::unique_ptr<detail::llm_text_encoder> text; std::unique_ptr<detail::llm_text_encoder> text;
@ -30,6 +32,7 @@ std::expected<std::unique_ptr<model>, std::string> model::load(std::string_view
auto state = std::make_unique<impl>(); auto state = std::make_unique<impl>();
state->motion = std::move(*file); state->motion = std::move(*file);
state->motion_path = std::string(motion_path); state->motion_path = std::string(motion_path);
state->skeleton = detail::find_skeleton(state->motion.strings.at("kimodo.skeleton"));
#ifdef KIMODO_HAVE_GGML #ifdef KIMODO_HAVE_GGML
if (!text_path.empty()) { if (!text_path.empty()) {
auto text = detail::llm_text_encoder::load(text_path); auto text = detail::llm_text_encoder::load(text_path);
@ -74,7 +77,8 @@ std::expected<motion_data, std::string> model::generate_embedding(
} }
std::mt19937_64 rng(seed); std::mt19937_64 rng(seed);
std::normal_distribution<float> normal(0.f, 1.f); std::normal_distribution<float> normal(0.f, 1.f);
std::vector<float> noise(static_cast<size_t>(frames)*273); const size_t motion_dim=impl_->skeleton->motion_dim();
std::vector<float> noise(static_cast<size_t>(frames)*motion_dim);
for (float &value : noise) value = normal(rng); for (float &value : noise) value = normal(rng);
auto sampled = detail::sample_motion_from_noise(*impl_->weights, noise, embedding, frames, steps, text_cfg, constraint_cfg); auto sampled = detail::sample_motion_from_noise(*impl_->weights, noise, embedding, frames, steps, text_cfg, constraint_cfg);
if (!sampled) return std::unexpected(sampled.error()); if (!sampled) return std::unexpected(sampled.error());
@ -84,10 +88,10 @@ std::expected<motion_data, std::string> model::generate_embedding(
if (!global_std) return std::unexpected(global_std.error()); if (!global_std) return std::unexpected(global_std.error());
if (!body_mean) return std::unexpected(body_mean.error()); if (!body_mean) return std::unexpected(body_mean.error());
if (!body_std) return std::unexpected(body_std.error()); if (!body_std) return std::unexpected(body_std.error());
auto decoded=detail::decode_smplx22(*sampled,frames,*global_mean,*global_std,*body_mean,*body_std); auto decoded=detail::decode_motion(*sampled,frames,*impl_->skeleton,*global_mean,*global_std,*body_mean,*body_std);
if (!decoded) return std::unexpected(decoded.error()); if (!decoded) return std::unexpected(decoded.error());
motion_data result; motion_data result;
result.frames=frames; result.joints=22; result.frames=frames; result.joints=static_cast<unsigned>(impl_->skeleton->joints());
result.local_rotations_xyzw=std::move(decoded->local_xyzw); result.local_rotations_xyzw=std::move(decoded->local_xyzw);
result.root_positions=std::move(decoded->root_positions); result.root_positions=std::move(decoded->root_positions);
return result; return result;
@ -127,22 +131,23 @@ std::expected<motion_data, std::string> model::generate_text_sequence(
const auto sampled_frames = static_cast<size_t>(segment.frames) + const auto sampled_frames = static_cast<size_t>(segment.frames) +
(index == 0 ? 0 : transition_frames); (index == 0 ? 0 : transition_frames);
embeddings.push_back(*embedding); embeddings.push_back(*embedding);
noise.emplace_back(sampled_frames*273); noise.emplace_back(sampled_frames*impl_->skeleton->motion_dim());
for (float &value : noise.back()) value=normal(rng); for (float &value : noise.back()) value=normal(rng);
sampled.push_back({embeddings.back(), noise.back(), segment.frames}); sampled.push_back({embeddings.back(), noise.back(), segment.frames});
} }
auto joined=detail::sample_motion_sequence_from_noise(*impl_->weights,sampled,transition_frames,steps,text_cfg,constraint_cfg); auto joined=detail::sample_motion_sequence_from_noise(*impl_->weights,sampled,transition_frames,steps,text_cfg,constraint_cfg);
if (!joined) return std::unexpected(joined.error()); if (!joined) return std::unexpected(joined.error());
const auto frames=static_cast<unsigned>(joined->size()/273); const size_t motion_dim=impl_->skeleton->motion_dim(), body_dim=impl_->skeleton->body_dim();
const auto frames=static_cast<unsigned>(joined->size()/motion_dim);
auto normalized=*joined; auto normalized=*joined;
for (size_t row=0; row<frames; ++row) { for (size_t row=0; row<frames; ++row) {
auto *value=normalized.data()+row*273; auto *value=normalized.data()+row*motion_dim;
for (size_t d=0; d<5; ++d) value[d]=(value[d]-(*gm)[d])/std::sqrt((*gs)[d]*(*gs)[d]+1.e-5F); for (size_t d=0; d<5; ++d) value[d]=(value[d]-(*gm)[d])/std::sqrt((*gs)[d]*(*gs)[d]+1.e-5F);
for (size_t d=0; d<268; ++d) value[5+d]=(value[5+d]-(*bm)[d])/std::sqrt((*bs)[d]*(*bs)[d]+1.e-5F); for (size_t d=0; d<body_dim; ++d) value[5+d]=(value[5+d]-(*bm)[d])/std::sqrt((*bs)[d]*(*bs)[d]+1.e-5F);
} }
auto decoded=detail::decode_smplx22(normalized,frames,*gm,*gs,*bm,*bs); auto decoded=detail::decode_motion(normalized,frames,*impl_->skeleton,*gm,*gs,*bm,*bs);
if (!decoded) return std::unexpected(decoded.error()); if (!decoded) return std::unexpected(decoded.error());
motion_data result; result.frames=frames; result.joints=22; motion_data result; result.frames=frames; result.joints=static_cast<unsigned>(impl_->skeleton->joints());
result.local_rotations_xyzw=std::move(decoded->local_xyzw); result.root_positions=std::move(decoded->root_positions); result.local_rotations_xyzw=std::move(decoded->local_xyzw); result.root_positions=std::move(decoded->root_positions);
return result; return result;
#else #else

View File

@ -1,13 +1,14 @@
#include "motion_decode.hpp" #include "motion_decode.hpp"
#include <array> #include "skeleton.hpp"
#include <cmath> #include <cmath>
#include <vector>
namespace kimodo::detail { namespace { namespace kimodo::detail { namespace {
constexpr int parent[22]={-1,0,0,0,1,2,3,4,5,6,7,8,9,9,9,12,13,14,16,17,18,19};
struct M{float v[9];}; struct M{float v[9];};
M mul(const M&a,const M&b){M r{};for(int i=0;i<3;++i)for(int j=0;j<3;++j)for(int k=0;k<3;++k)r.v[i*3+j]+=a.v[i*3+k]*b.v[k*3+j];return r;} M mul(const M&a,const M&b){M r{};for(int i=0;i<3;++i)for(int j=0;j<3;++j)for(int k=0;k<3;++k)r.v[i*3+j]+=a.v[i*3+k]*b.v[k*3+j];return r;}
M tr(const M&a){M r{};for(int i=0;i<3;++i)for(int j=0;j<3;++j)r.v[i*3+j]=a.v[j*3+i];return r;} M tr(const M&a){M r{};for(int i=0;i<3;++i)for(int j=0;j<3;++j)r.v[i*3+j]=a.v[j*3+i];return r;}
M six(const float*x){float n=std::sqrt(x[0]*x[0]+x[1]*x[1]+x[2]*x[2]);float a[3]={x[0]/n,x[1]/n,x[2]/n};float z[3]={a[1]*x[5]-a[2]*x[4],a[2]*x[3]-a[0]*x[5],a[0]*x[4]-a[1]*x[3]};n=std::sqrt(z[0]*z[0]+z[1]*z[1]+z[2]*z[2]);for(float&v:z)v/=n;float b[3]={z[1]*a[2]-z[2]*a[1],z[2]*a[0]-z[0]*a[2],z[0]*a[1]-z[1]*a[0]};return M{{a[0],b[0],z[0],a[1],b[1],z[1],a[2],b[2],z[2]}};} M six(const float*x){float n=std::sqrt(x[0]*x[0]+x[1]*x[1]+x[2]*x[2]);float a[3]={x[0]/n,x[1]/n,x[2]/n};float z[3]={a[1]*x[5]-a[2]*x[4],a[2]*x[3]-a[0]*x[5],a[0]*x[4]-a[1]*x[3]};n=std::sqrt(z[0]*z[0]+z[1]*z[1]+z[2]*z[2]);for(float&v:z)v/=n;float b[3]={z[1]*a[2]-z[2]*a[1],z[2]*a[0]-z[0]*a[2],z[0]*a[1]-z[1]*a[0]};return M{{a[0],b[0],z[0],a[1],b[1],z[1],a[2],b[2],z[2]}};}
void quat(const M&m,float*q){float w,x,y,z,t=m.v[0]+m.v[4]+m.v[8];if(t>0){float s=2*std::sqrt(t+1);w=.25f*s;x=(m.v[7]-m.v[5])/s;y=(m.v[2]-m.v[6])/s;z=(m.v[3]-m.v[1])/s;}else if(m.v[0]>m.v[4]&&m.v[0]>m.v[8]){float s=2*std::sqrt(1+m.v[0]-m.v[4]-m.v[8]);w=(m.v[7]-m.v[5])/s;x=.25f*s;y=(m.v[1]+m.v[3])/s;z=(m.v[2]+m.v[6])/s;}else if(m.v[4]>m.v[8]){float s=2*std::sqrt(1+m.v[4]-m.v[0]-m.v[8]);w=(m.v[2]-m.v[6])/s;x=(m.v[1]+m.v[3])/s;y=.25f*s;z=(m.v[5]+m.v[7])/s;}else{float s=2*std::sqrt(1+m.v[8]-m.v[0]-m.v[4]);w=(m.v[3]-m.v[1])/s;x=(m.v[2]+m.v[6])/s;y=(m.v[5]+m.v[7])/s;z=.25f*s;}q[0]=x;q[1]=y;q[2]=z;q[3]=w;} void quat(const M&m,float*q){float w,x,y,z,t=m.v[0]+m.v[4]+m.v[8];if(t>0){float s=2*std::sqrt(t+1);w=.25f*s;x=(m.v[7]-m.v[5])/s;y=(m.v[2]-m.v[6])/s;z=(m.v[3]-m.v[1])/s;}else if(m.v[0]>m.v[4]&&m.v[0]>m.v[8]){float s=2*std::sqrt(1+m.v[0]-m.v[4]-m.v[8]);w=(m.v[7]-m.v[5])/s;x=.25f*s;y=(m.v[1]+m.v[3])/s;z=(m.v[2]+m.v[6])/s;}else if(m.v[4]>m.v[8]){float s=2*std::sqrt(1+m.v[4]-m.v[0]-m.v[8]);w=(m.v[2]-m.v[6])/s;x=(m.v[1]+m.v[3])/s;y=.25f*s;z=(m.v[5]+m.v[7])/s;}else{float s=2*std::sqrt(1+m.v[8]-m.v[0]-m.v[4]);w=(m.v[3]-m.v[1])/s;x=(m.v[2]+m.v[6])/s;y=(m.v[5]+m.v[7])/s;z=.25f*s;}q[0]=x;q[1]=y;q[2]=z;q[3]=w;}
} }
std::expected<decoded_motion,std::string> decode_smplx22(std::span<const float>x,size_t T,std::span<const float>gm,std::span<const float>gs,std::span<const float>bm,std::span<const float>bs){if(x.size()!=T*273||gm.size()!=5||gs.size()!=5||bm.size()!=268||bs.size()!=268)return std::unexpected("invalid SMPL-X decode inputs");decoded_motion o;o.local_xyzw.resize(T*22*4);o.root_positions.resize(T*3);auto scale=[](float s){return std::sqrt(s*s+1.e-5f);};for(size_t t=0;t<T;++t){const float*in=x.data()+t*273;std::array<float,273> f{};for(int i=0;i<5;++i)f[i]=in[i]*scale(gs[i])+gm[i];for(int i=0;i<268;++i)f[5+i]=in[5+i]*scale(bs[i])+bm[i];o.root_positions[t*3]=f[0]+f[5];o.root_positions[t*3+1]=f[6];o.root_positions[t*3+2]=f[2]+f[7];M g[22],l[22];for(int j=0;j<22;++j)g[j]=six(f.data()+71+j*6);for(int j=0;j<22;++j)l[j]=parent[j]<0?g[j]:mul(tr(g[parent[j]]),g[j]);for(int j=0;j<22;++j)quat(l[j],o.local_xyzw.data()+(t*22+j)*4);}return o;} std::expected<decoded_motion,std::string> decode_motion(std::span<const float>x,size_t T,const skeleton_spec&s,std::span<const float>gm,std::span<const float>gs,std::span<const float>bm,std::span<const float>bs){const size_t D=s.motion_dim(),J=s.joints(),body=D-5,rotation=5+3*J;if(x.size()!=T*D||gm.size()!=5||gs.size()!=5||bm.size()!=body||bs.size()!=body)return std::unexpected("invalid "+std::string(s.key)+" decode inputs");decoded_motion o;o.local_xyzw.resize(T*J*4);o.root_positions.resize(T*3);auto scale=[](float v){return std::sqrt(v*v+1.e-5f);};std::vector<float>f(D);std::vector<M>g(J),l(J);for(size_t t=0;t<T;++t){const float*in=x.data()+t*D;for(size_t i=0;i<5;++i)f[i]=in[i]*scale(gs[i])+gm[i];for(size_t i=0;i<body;++i)f[5+i]=in[5+i]*scale(bs[i])+bm[i];o.root_positions[t*3]=f[0]+f[5];o.root_positions[t*3+1]=f[6];o.root_positions[t*3+2]=f[2]+f[7];for(size_t j=0;j<J;++j)g[j]=six(f.data()+rotation+j*6);for(size_t j=0;j<J;++j)l[j]=s.parents[j]<0?g[j]:mul(tr(g[static_cast<size_t>(s.parents[j])]),g[j]);for(size_t j=0;j<J;++j)quat(l[j],o.local_xyzw.data()+(t*J+j)*4);}return o;}
std::expected<decoded_motion,std::string> decode_smplx22(std::span<const float>x,size_t T,std::span<const float>gm,std::span<const float>gs,std::span<const float>bm,std::span<const float>bs){return decode_motion(x,T,smplx22_spec,gm,gs,bm,bs);}
} }

View File

@ -4,6 +4,8 @@
#include <string> #include <string>
#include <vector> #include <vector>
namespace kimodo::detail { namespace kimodo::detail {
struct skeleton_spec;
struct decoded_motion { std::vector<float> local_xyzw, root_positions; }; struct decoded_motion { std::vector<float> local_xyzw, root_positions; };
std::expected<decoded_motion,std::string> decode_motion(std::span<const float> normalized, std::size_t frames, const skeleton_spec &skeleton, std::span<const float> global_mean, std::span<const float> global_std, std::span<const float> body_mean, std::span<const float> body_std);
std::expected<decoded_motion,std::string> decode_smplx22(std::span<const float> normalized, std::size_t frames, std::span<const float> global_mean, std::span<const float> global_std, std::span<const float> body_mean, std::span<const float> body_std); std::expected<decoded_motion,std::string> decode_smplx22(std::span<const float> normalized, std::size_t frames, std::span<const float> global_mean, std::span<const float> global_std, std::span<const float> body_mean, std::span<const float> body_std);
} }

View File

@ -3,6 +3,7 @@
#include "denoiser.hpp" #include "denoiser.hpp"
#include "ggml_weights.hpp" #include "ggml_weights.hpp"
#include "motion_decode.hpp" #include "motion_decode.hpp"
#include "skeleton.hpp"
#include <algorithm> #include <algorithm>
#include <cmath> #include <cmath>
@ -54,10 +55,11 @@ int main(int argc, char **argv) try {
const std::filesystem::path fixture(argv[2]), output(argv[5]); const std::filesystem::path fixture(argv[2]), output(argv[5]);
const auto embedding = read_f32(fixture / "text_features.f32"); const auto embedding = read_f32(fixture / "text_features.f32");
const auto noise = read_f32(fixture / "sampling_initial_noise.f32"); const auto noise = read_f32(fixture / "sampling_initial_noise.f32");
if (embedding.size() != 4096 || noise.size() != frames * 273)
throw std::runtime_error("fixture does not match requested [1,1,4096] embedding and [1,T,273] noise");
auto weights = kimodo::detail::ggml_motion_weights::load(argv[1]); auto weights = kimodo::detail::ggml_motion_weights::load(argv[1]);
if (!weights) throw std::runtime_error(weights.error()); if (!weights) throw std::runtime_error(weights.error());
const auto *skeleton=kimodo::detail::find_skeleton((*weights)->skeleton_key());
if (!skeleton || embedding.size() != 4096 || noise.size() != frames * skeleton->motion_dim())
throw std::runtime_error("fixture does not match the requested embedding and model motion dimensions");
auto sampled = kimodo::detail::sample_motion_from_noise(**weights, noise, embedding, frames, steps, 2.f, 2.f); auto sampled = kimodo::detail::sample_motion_from_noise(**weights, noise, embedding, frames, steps, 2.f, 2.f);
if (!sampled) throw std::runtime_error(sampled.error()); if (!sampled) throw std::runtime_error(sampled.error());
auto gm = (**weights).f32_values("stats.global_root.mean"); auto gm = (**weights).f32_values("stats.global_root.mean");
@ -65,7 +67,7 @@ int main(int argc, char **argv) try {
auto bm = (**weights).f32_values("stats.body.mean"); auto bm = (**weights).f32_values("stats.body.mean");
auto bs = (**weights).f32_values("stats.body.std"); auto bs = (**weights).f32_values("stats.body.std");
if (!gm || !gs || !bm || !bs) throw std::runtime_error("missing motion normalisation tensors"); if (!gm || !gs || !bm || !bs) throw std::runtime_error("missing motion normalisation tensors");
auto decoded = kimodo::detail::decode_smplx22(*sampled, frames, *gm, *gs, *bm, *bs); auto decoded = kimodo::detail::decode_motion(*sampled, frames, *skeleton, *gm, *gs, *bm, *bs);
if (!decoded) throw std::runtime_error(decoded.error()); if (!decoded) throw std::runtime_error(decoded.error());
std::filesystem::create_directories(output); std::filesystem::create_directories(output);
write_f32(output / "sampling_final_state.f32", *sampled); write_f32(output / "sampling_final_state.f32", *sampled);

View File

@ -1,132 +1,72 @@
#include "denoiser.hpp" #include "denoiser.hpp"
#include "ggml_weights.hpp" #include "ggml_weights.hpp"
#include "skeleton.hpp"
#include <algorithm> #include <algorithm>
#include <array>
#include <cmath> #include <cmath>
#include <vector>
namespace kimodo::detail { namespace kimodo::detail {
namespace { namespace {
constexpr int parent[22]={-1,0,0,0,1,2,3,4,5,6,7,8,9,9,9,12,13,14,16,17,18,19};
constexpr float offset[22][3]={{0,0,0},{.052299179F,-.093935639F,-.027606763F},{-.057192899F,-.106548190F,-.022217851F},{-.001495834F,.112929940F,-.024981268F},{.058866613F,-.416441321F,-.006556974F},{-.048074268F,-.397559673F,-.014061437F},{.006900469F,.145636231F,-.006858510F},{-.041737989F,-.437583506F,-.029511765F},{.014489345F,-.446852267F,-.018029511F},{-.010334037F,.056081813F,.021115851F},{.049293540F,-.065279245F,.126259089F},{-.040575184F,-.065286517F,.127075911F},{-.011025756F,.171365142F,-.028827066F},{.047724526F,.087643057F,-.008375450F},{-.046636276F,.086612143F,-.014864366F},{.024654359F,.175390735F,.024463326F},{.126284808F,.057680372F,-.013885141F},{-.109341696F,.053674292F,-.009117880F},{.272907287F,-.069853373F,-.039094493F},{-.292028785F,-.035440356F,-.024564851F},{.276173830F,.021254137F,-.002478220F},{-.271878421F,-.004834589F,-.016445294F}};
struct mat { double v[9]; }; struct mat { double v[9]; };
mat mul(const mat&a,const mat&b){mat r{};for(int i=0;i<3;++i)for(int j=0;j<3;++j)for(int k=0;k<3;++k)r.v[i*3+j]+=a.v[i*3+k]*b.v[k*3+j];return r;} mat mul(const mat&a,const mat&b){mat r{};for(int i=0;i<3;++i)for(int j=0;j<3;++j)for(int k=0;k<3;++k)r.v[i*3+j]+=a.v[i*3+k]*b.v[k*3+j];return r;}
mat trans(const mat&a){mat r{};for(int i=0;i<3;++i)for(int j=0;j<3;++j)r.v[i*3+j]=a.v[j*3+i];return r;} mat trans(const mat&a){mat r{};for(int i=0;i<3;++i)for(int j=0;j<3;++j)r.v[i*3+j]=a.v[j*3+i];return r;}
mat cont6(const float*x){double a[3]={x[0],x[1],x[2]},n=std::sqrt(a[0]*a[0]+a[1]*a[1]+a[2]*a[2]);for(double&q:a)q/=n;double z[3]={a[1]*x[5]-a[2]*x[4],a[2]*x[3]-a[0]*x[5],a[0]*x[4]-a[1]*x[3]};n=std::sqrt(z[0]*z[0]+z[1]*z[1]+z[2]*z[2]);for(double&q:z)q/=n;double b[3]={z[1]*a[2]-z[2]*a[1],z[2]*a[0]-z[0]*a[2],z[0]*a[1]-z[1]*a[0]};return{{a[0],b[0],z[0],a[1],b[1],z[1],a[2],b[2],z[2]}};} mat cont6(const float*x){double a[3]={x[0],x[1],x[2]},n=std::sqrt(a[0]*a[0]+a[1]*a[1]+a[2]*a[2]);for(double&q:a)q/=n;double z[3]={a[1]*x[5]-a[2]*x[4],a[2]*x[3]-a[0]*x[5],a[0]*x[4]-a[1]*x[3]};n=std::sqrt(z[0]*z[0]+z[1]*z[1]+z[2]*z[2]);for(double&q:z)q/=n;double b[3]={z[1]*a[2]-z[2]*a[1],z[2]*a[0]-z[0]*a[2],z[0]*a[1]-z[1]*a[0]};return{{a[0],b[0],z[0],a[1],b[1],z[1],a[2],b[2],z[2]}};}
void rotate(const mat&m,const float *x,float *o){for(int i=0;i<3;++i)o[i]=static_cast<float>(m.v[i*3]*x[0]+m.v[i*3+1]*x[1]+m.v[i*3+2]*x[2]);} void rotate(const mat&m,const std::array<float,3>&x,float*o){for(int i=0;i<3;++i)o[i]=static_cast<float>(m.v[i*3]*x[0]+m.v[i*3+1]*x[1]+m.v[i*3+2]*x[2]);}
// Reconstruct the full-body/end-effector condition used by upstream
// `_multiprompt`, generalized over the three released skeleton layouts.
float condition_row(const float *raw, const skeleton_spec &s, float *value) {
const size_t D=s.motion_dim(),J=s.joints(),rotation_begin=5+3*J;
std::copy_n(raw,D,value);
std::vector<mat> decoded(J),local(J),global(J);
for(size_t j=0;j<J;++j)decoded[j]=cont6(value+rotation_begin+j*6);
for(size_t j=0;j<J;++j)local[j]=s.parents[j]<0?decoded[j]:mul(trans(decoded[static_cast<size_t>(s.parents[j])]),decoded[j]);
const float root[3]={value[0]+value[5],value[6],value[2]+value[7]};
std::vector<std::array<float,3>> posed(J);
for(size_t j=0;j<J;++j){const int parent=s.parents[j];if(parent<0){global[j]=local[j];posed[j]={root[0],root[1],root[2]};}else{global[j]=mul(global[static_cast<size_t>(parent)],local[j]);float offset[3];rotate(global[static_cast<size_t>(parent)],s.offsets[j],offset);for(int k=0;k<3;++k)posed[j][k]=posed[static_cast<size_t>(parent)][k]+offset[k];}}
const auto right=s.hips[0],left=s.hips[1];
const float angle=std::atan2(posed[right][2]-posed[left][2],-(posed[right][0]-posed[left][0]));
value[1]=root[1];value[3]=std::cos(angle);value[4]=std::sin(angle);
for(size_t j=0;j<J;++j){value[5+j*3]=posed[j][0]-value[0];value[6+j*3]=posed[j][1];value[7+j*3]=posed[j][2]-value[2];}
for(unsigned joint:s.end_effectors)for(int d=0;d<6;++d)value[rotation_begin+joint*6+static_cast<size_t>(d)]=static_cast<float>(global[joint].v[(d%3)*3+d/3]);
return angle;
} }
}
std::expected<sequence_transition, std::string> prepare_sequence_transition( std::expected<sequence_transition, std::string> prepare_sequence_transition(
const ggml_motion_weights &, std::span<const float> previous, const ggml_motion_weights &weights, std::span<const float> previous,
std::size_t continuation_frames, unsigned transition_frames) { std::size_t continuation_frames, unsigned transition_frames) {
constexpr size_t features = 273; const auto *s=find_skeleton(weights.skeleton_key());
const size_t overlap=transition_frames; if(!s)return std::unexpected("unsupported sequence skeleton");
if (!overlap || overlap>=continuation_frames || previous.size()<=(overlap*features)) const size_t D=s->motion_dim(),J=s->joints(),rotation_begin=5+3*J,rotation_end=rotation_begin+6*J,overlap=transition_frames;
if(!overlap||overlap>=continuation_frames||previous.size()<=overlap*D||previous.size()%D)
return std::unexpected("invalid sequence transition"); return std::unexpected("invalid sequence transition");
sequence_transition result; sequence_transition result;
result.observed.resize((continuation_frames+overlap)*features); result.observed.resize((continuation_frames+overlap)*D);
result.observed_mask.resize(result.observed.size()); result.observed_mask.resize(result.observed.size());
const size_t previous_start=previous.size()-overlap*features; const size_t previous_start=previous.size()-overlap*D;
constexpr std::array<std::pair<size_t, size_t>, 3> constrained = {{{0, 71}, {113, 125}, {191, 203}}}; std::vector<float> value(D);
for (size_t frame=0; frame<overlap; ++frame) { for(size_t frame=0;frame<overlap;++frame){const size_t base=frame*D;condition_row(previous.data()+previous_start+base,*s,value.data());std::copy_n(value.data(),rotation_end,result.observed.data()+base);std::fill(result.observed_mask.begin()+static_cast<std::ptrdiff_t>(base),result.observed_mask.begin()+static_cast<std::ptrdiff_t>(base+rotation_begin),1.F);for(unsigned joint:s->end_effectors){const size_t first=base+rotation_begin+joint*6;std::fill(result.observed_mask.begin()+static_cast<std::ptrdiff_t>(first),result.observed_mask.begin()+static_cast<std::ptrdiff_t>(first+6),1.F);}}
const size_t base=frame*features; result.origin_x=result.observed[0];result.origin_z=result.observed[2];
std::array<float, features> value{}; for(size_t frame=0;frame<overlap;++frame){auto*row=result.observed.data()+frame*D;row[0]-=result.origin_x;row[2]-=result.origin_z;}
const float *raw=previous.data()+previous_start+base; result.first_heading=condition_row(previous.data()+previous_start,*s,value.data());
std::copy_n(raw,features,value.data());
mat decoded[22],local[22],global[22]; for(int j=0;j<22;++j)decoded[j]=cont6(value.data()+71+j*6);
for(int j=0;j<22;++j) local[j]=parent[j]<0?decoded[j]:mul(trans(decoded[parent[j]]),decoded[j]);
float root[3]={value[0]+value[5],value[6],value[2]+value[7]}, posed[22][3]{};
for(int j=0;j<22;++j){if(parent[j]<0){global[j]=local[j];posed[j][0]=root[0];posed[j][1]=root[1];posed[j][2]=root[2];}else{global[j]=mul(global[parent[j]],local[j]);float d[3];rotate(global[parent[j]],offset[j],d);for(int k=0;k<3;++k)posed[j][k]=posed[parent[j]][k]+d[k];}}
// FullBodyConstraintSet: smooth root, root Y, heading, all joint
// positions; EndEffectorConstraintSet adds its four rotation blocks.
value[0]=value[0]; value[1]=root[1]; value[2]=value[2];
// `compute_heading_angle`: right hip minus left hip.
const float dx=posed[2][0]-posed[1][0], dz=posed[2][2]-posed[1][2], angle=std::atan2(dz,-dx);
value[3]=std::cos(angle); value[4]=std::sin(angle);
for(int j=0;j<22;++j){value[5+j*3]=posed[j][0]-value[0];value[6+j*3]=posed[j][1];value[7+j*3]=posed[j][2]-value[2];}
for(int j: {7,8,20,21}) for(int d=0;d<6;++d)
value[71+j*6+d]=static_cast<float>(global[j].v[(d%3)*3+d/3]);
std::copy_n(value.data(),203,result.observed.data()+base);
for (const auto &[first,last] : constrained)
std::fill(result.observed_mask.begin()+static_cast<std::ptrdiff_t>(base+first),
result.observed_mask.begin()+static_cast<std::ptrdiff_t>(base+last),1.F);
}
result.origin_x=result.observed[0];
result.origin_z=result.observed[2];
for (size_t frame=0; frame<overlap; ++frame) {
auto *row=result.observed.data()+frame*features;
row[0]-=result.origin_x;
row[2]-=result.origin_z;
}
// First heading comes from the first retained full-body constraint.
const size_t first=(previous.size()-overlap*features);
const float *raw=previous.data()+first;
std::array<float, features> value{}; std::copy_n(raw,features,value.data());
mat decoded[22],local[22],global[22]; for(int j=0;j<22;++j)decoded[j]=cont6(value.data()+71+j*6);
for(int j=0;j<22;++j)local[j]=parent[j]<0?decoded[j]:mul(trans(decoded[parent[j]]),decoded[j]);
float root[3]={value[0]+value[5],value[6],value[2]+value[7]}, posed[22][3]{};
for(int j=0;j<22;++j){if(parent[j]<0){global[j]=local[j];for(int k=0;k<3;++k)posed[j][k]=root[k];}else{global[j]=mul(global[parent[j]],local[j]);float d[3];rotate(global[parent[j]],offset[j],d);for(int k=0;k<3;++k)posed[j][k]=posed[parent[j]][k]+d[k];}}
result.first_heading=std::atan2(posed[2][2]-posed[1][2],-(posed[2][0]-posed[1][0]));
return result; return result;
} }
std::expected<std::vector<float>, std::string> sample_motion_sequence_from_noise( std::expected<std::vector<float>, std::string> sample_motion_sequence_from_noise(
const ggml_motion_weights &weights, std::span<const sampled_sequence_segment> segments, const ggml_motion_weights &weights, std::span<const sampled_sequence_segment> segments,
unsigned transition_frames, unsigned steps, float text_weight, float constraint_weight) { unsigned transition_frames, unsigned steps, float text_weight, float constraint_weight) {
constexpr size_t features = 273; const size_t D=weights.motion_dim(),body=D-5;
if (segments.empty() || !transition_frames) if(segments.empty()||!transition_frames||!D)return std::unexpected("sequence requires segments and a transition");
return std::unexpected("sequence requires segments and a transition");
auto gm=weights.f32_values("stats.global_root.mean"),gs=weights.f32_values("stats.global_root.std"); auto gm=weights.f32_values("stats.global_root.mean"),gs=weights.f32_values("stats.global_root.std");
auto bm=weights.f32_values("stats.body.mean"),bs=weights.f32_values("stats.body.std"); auto bm=weights.f32_values("stats.body.mean"),bs=weights.f32_values("stats.body.std");
if (!gm || !gs || !bm || !bs) return std::unexpected("motion GGUF lacks motion statistics"); if(!gm||!gs||!bm||!bs||gm->size()!=5||gs->size()!=5||bm->size()!=body||bs->size()!=body)return std::unexpected("motion GGUF lacks compatible motion statistics");
// Upstream Stats normalizes with sqrt(std^2 + 1e-5), rather than raw std.
auto scale=[](float stddev){return std::sqrt(stddev*stddev+1.e-5F);}; auto scale=[](float stddev){return std::sqrt(stddev*stddev+1.e-5F);};
auto unnormalize = [&](std::vector<float> &motion) { for(size_t row=0;row<motion.size()/features;++row) { auto *v=motion.data()+row*features; for(size_t d=0;d<5;++d)v[d]=v[d]*scale((*gs)[d])+(*gm)[d]; for(size_t d=0;d<268;++d)v[5+d]=v[5+d]*scale((*bs)[d])+(*bm)[d]; } }; auto unnormalize=[&](std::vector<float>&motion){for(size_t row=0;row<motion.size()/D;++row){auto*v=motion.data()+row*D;for(size_t d=0;d<5;++d)v[d]=v[d]*scale((*gs)[d])+(*gm)[d];for(size_t d=0;d<body;++d)v[5+d]=v[5+d]*scale((*bs)[d])+(*bm)[d];}};
auto normalize = [&](std::vector<float> &motion) { for(size_t row=0;row<motion.size()/features;++row) { auto *v=motion.data()+row*features; for(size_t d=0;d<5;++d)v[d]=(v[d]-(*gm)[d])/scale((*gs)[d]); for(size_t d=0;d<268;++d)v[5+d]=(v[5+d]-(*bm)[d])/scale((*bs)[d]); } }; auto normalize=[&](std::vector<float>&motion){for(size_t row=0;row<motion.size()/D;++row){auto*v=motion.data()+row*D;for(size_t d=0;d<5;++d)v[d]=(v[d]-(*gm)[d])/scale((*gs)[d]);for(size_t d=0;d<body;++d)v[5+d]=(v[5+d]-(*bm)[d])/scale((*bs)[d]);}};
std::vector<float> joined,previous; std::vector<float> joined,previous;
for (size_t index=0; index<segments.size(); ++index) { for(size_t index=0;index<segments.size();++index){const auto&segment=segments[index];const size_t sampled_frames=segment.frames+(index?transition_frames:0);if(segment.frames<2||segment.embedding.size()!=4096||segment.initial_noise.size()!=sampled_frames*D)return std::unexpected("invalid sampled sequence segment");std::vector<float>current;if(!index){auto sampled=sample_motion_from_noise(weights,segment.initial_noise,segment.embedding,sampled_frames,steps,text_weight,constraint_weight);if(!sampled)return std::unexpected(sampled.error());current=std::move(*sampled);unnormalize(current);}else{const size_t overlap=transition_frames;if(overlap>=segment.frames||previous.size()<overlap*D)return std::unexpected("transition must be shorter than every following segment");auto transition=prepare_sequence_transition(weights,previous,segment.frames,transition_frames);if(!transition)return std::unexpected(transition.error());const float origin_x=transition->origin_x,origin_z=transition->origin_z;normalize(transition->observed);auto sampled=sample_motion_from_noise_conditioned(weights,segment.initial_noise,segment.embedding,transition->observed,transition->observed_mask,transition->first_heading,sampled_frames,steps,text_weight,constraint_weight);if(!sampled)return std::unexpected(sampled.error());current=std::move(*sampled);unnormalize(current);for(size_t frame=0;frame<sampled_frames;++frame){auto*row=current.data()+frame*D;row[0]+=origin_x;row[2]+=origin_z;}const size_t start=joined.size()-overlap*D;for(size_t frame=0;frame<overlap;++frame){const float alpha=overlap==1?.5F:1.F-float(frame)/float(overlap-1);for(size_t d=0;d<D;++d)joined[start+frame*D+d]=alpha*joined[start+frame*D+d]+(1.F-alpha)*current[frame*D+d];}joined.insert(joined.end(),current.begin()+static_cast<std::ptrdiff_t>(overlap*D),current.end());}if(!index)joined=current;previous=std::move(current);}
const auto &segment=segments[index];
const size_t sampled_frames=segment.frames+(index ? transition_frames : 0);
if (segment.frames < 2 || segment.embedding.size()!=4096 ||
segment.initial_noise.size()!=sampled_frames*features)
return std::unexpected("invalid sampled sequence segment");
std::vector<float> current;
if (!index) {
auto sampled=sample_motion_from_noise(weights,segment.initial_noise,segment.embedding,
sampled_frames,steps,text_weight,constraint_weight);
if (!sampled) return std::unexpected(sampled.error());
current=std::move(*sampled);
unnormalize(current);
} else {
const size_t overlap=transition_frames;
if (overlap >= segment.frames || previous.size()<overlap*features)
return std::unexpected("transition must be shorter than every following segment");
auto transition=prepare_sequence_transition(weights,previous,segment.frames,transition_frames);
if (!transition) return std::unexpected(transition.error());
const float origin_x=transition->origin_x;
const float origin_z=transition->origin_z;
normalize(transition->observed);
auto sampled=sample_motion_from_noise_conditioned(weights,segment.initial_noise,segment.embedding,
transition->observed,transition->observed_mask,transition->first_heading,sampled_frames,steps,text_weight,constraint_weight);
if (!sampled) return std::unexpected(sampled.error());
current=std::move(*sampled);
unnormalize(current);
for (size_t frame=0; frame<sampled_frames; ++frame) {
auto *row=current.data()+frame*features;
row[0]+=origin_x;
row[2]+=origin_z;
}
const size_t start=joined.size()-overlap*features;
for (size_t frame=0; frame<overlap; ++frame) {
const float alpha=overlap==1?.5F:1.F-float(frame)/float(overlap-1);
for (size_t d=0; d<features; ++d)
joined[start+frame*features+d]=alpha*joined[start+frame*features+d]+(1.F-alpha)*current[frame*features+d];
}
joined.insert(joined.end(),current.begin()+static_cast<std::ptrdiff_t>(overlap*features),current.end());
}
if (!index) joined=current;
previous=std::move(current);
}
return joined; return joined;
} }
} // namespace kimodo::detail } // namespace kimodo::detail

101
src/skeleton.hpp Normal file
View File

@ -0,0 +1,101 @@
#pragma once
#include <array>
#include <cstddef>
#include <span>
#include <string_view>
namespace kimodo::detail {
// Joint names and parent graphs are copied from NVIDIA Kimodo's Apache-2.0
// kimodo/skeleton/definitions.py. Parent-local offsets were extracted from
// the accompanying joints.p assets in the trusted reference container.
struct skeleton_spec {
std::string_view key;
std::span<const std::string_view> names;
std::span<const int> parents;
std::span<const std::array<float, 3>> offsets;
std::array<unsigned, 2> hips; // right, left
std::array<unsigned, 4> end_effectors; // left foot, right foot, left hand, right hand
[[nodiscard]] constexpr std::size_t joints() const noexcept { return names.size(); }
[[nodiscard]] constexpr std::size_t motion_dim() const noexcept { return 9 + 12 * joints(); }
[[nodiscard]] constexpr std::size_t body_dim() const noexcept { return motion_dim() - 5; }
};
inline constexpr std::array<std::string_view,22> smplx22_names{
std::string_view{"pelvis"}, "left_hip", "right_hip", "spine1", "left_knee", "right_knee",
"spine2", "left_ankle", "right_ankle", "spine3", "left_foot", "right_foot", "neck",
"left_collar", "right_collar", "head", "left_shoulder", "right_shoulder", "left_elbow",
"right_elbow", "left_wrist", "right_wrist"};
inline constexpr std::array smplx22_parents{-1,0,0,0,1,2,3,4,5,6,7,8,9,9,9,12,13,14,16,17,18,19};
inline constexpr std::array<std::array<float,3>,22> smplx22_offsets{{
{0,0,0},{.052299179F,-.093935639F,-.027606763F},{-.057192899F,-.106548190F,-.022217851F},
{-.001495834F,.112929940F,-.024981268F},{.058866613F,-.416441321F,-.006556974F},
{-.048074268F,-.397559673F,-.014061437F},{.006900469F,.145636231F,-.006858510F},
{-.041737989F,-.437583506F,-.029511765F},{.014489345F,-.446852267F,-.018029511F},
{-.010334037F,.056081813F,.021115851F},{.049293540F,-.065279245F,.126259089F},
{-.040575184F,-.065286517F,.127075911F},{-.011025756F,.171365142F,-.028827066F},
{.047724526F,.087643057F,-.008375450F},{-.046636276F,.086612143F,-.014864366F},
{.024654359F,.175390735F,.024463326F},{.126284808F,.057680372F,-.013885141F},
{-.109341696F,.053674292F,-.009117880F},{.272907287F,-.069853373F,-.039094493F},
{-.292028785F,-.035440356F,-.024564851F},{.276173830F,.021254137F,-.002478220F},
{-.271878421F,-.004834589F,-.016445294F}}};
inline constexpr std::array<std::string_view,30> soma30_names{
std::string_view{"Hips"}, "Spine1", "Spine2", "Chest", "Neck1", "Neck2", "Head", "Jaw",
"LeftEye", "RightEye", "LeftShoulder", "LeftArm", "LeftForeArm", "LeftHand",
"LeftHandThumbEnd", "LeftHandMiddleEnd", "RightShoulder", "RightArm", "RightForeArm",
"RightHand", "RightHandThumbEnd", "RightHandMiddleEnd", "LeftLeg", "LeftShin", "LeftFoot",
"LeftToeBase", "RightLeg", "RightShin", "RightFoot", "RightToeBase"};
inline constexpr std::array soma30_parents{-1,0,1,2,3,4,5,6,6,6,3,10,11,12,13,13,3,16,17,18,19,19,0,22,23,24,0,26,27,28};
inline constexpr std::array<std::array<float,3>,30> soma30_offsets{{
{0,0,0},{-.00013727F,.0500376256F,-.00053726669F},{-1.86574103e-9F,.0712530139F,-.000298248546F},
{-5.75188398e-9F,.0755006305F,-.00815970992F},{-.00181676517F,.263112953F,-.00553348292F},
{-2.85102231e-8F,.0770939664F,.0230258546F},{-4.5975437e-8F,.0612891595F,.0195370861F},
{2.63687901e-5F,.0047559225F,.0309494062F},{.0320638079F,.0538020513F,.0758688308F},
{-.0322244017F,.05361869F,.0755823359F},{.0162165175F,.232371641F,.0511341324F},
{.149198457F,2.19397873e-8F,-.0550232576F},{.287393078F,2.50268389e-9F,-2.58787737e-5F},
{.270939812F,-7.06625108e-9F,2.60897248e-5F},{.122686267F,-.0322017573F,.0483306876F},
{.190119595F,-.00312878387F,-.000339570373F},{-.0138011824F,.231803086F,.0521415786F},
{-.150371962F,1.17387901e-7F,-.0554560437F},{-.287366393F,1.87628082e-8F,-2.59709359e-5F},
{-.271336198F,-1.16767401e-9F,2.61269368e-5F},{-.122642483F,-.0321145448F,.0480403904F},
{-.190005945F,-.00306615542F,-.0003157343F},{.10043214F,-.0843452671F,.0259565473F},
{-1e-8F,-.432217537F,-.00802912805F},{1e-8F,-.421550959F,-.0348152298F},
{0,-.0505947206F,.132315294F},{-.10047278F,-.0829525995F,.0262031695F},
{1e-8F,-.433622059F,-.00805555828F},{2e-8F,-.421173943F,-.0347839785F},
{-3.42907669e-9F,-.0507960932F,.132841956F}}};
inline constexpr std::array<std::string_view,34> g1skel34_names{
std::string_view{"pelvis_skel"}, "left_hip_pitch_skel", "left_hip_roll_skel", "left_hip_yaw_skel",
"left_knee_skel", "left_ankle_pitch_skel", "left_ankle_roll_skel", "left_toe_base",
"right_hip_pitch_skel", "right_hip_roll_skel", "right_hip_yaw_skel", "right_knee_skel",
"right_ankle_pitch_skel", "right_ankle_roll_skel", "right_toe_base", "waist_yaw_skel",
"waist_roll_skel", "waist_pitch_skel", "left_shoulder_pitch_skel", "left_shoulder_roll_skel",
"left_shoulder_yaw_skel", "left_elbow_skel", "left_wrist_roll_skel", "left_wrist_pitch_skel",
"left_wrist_yaw_skel", "left_hand_roll_skel", "right_shoulder_pitch_skel",
"right_shoulder_roll_skel", "right_shoulder_yaw_skel", "right_elbow_skel",
"right_wrist_roll_skel", "right_wrist_pitch_skel", "right_wrist_yaw_skel", "right_hand_roll_skel"};
inline constexpr std::array g1skel34_parents{-1,0,1,2,3,4,5,6,0,8,9,10,11,12,13,0,15,16,17,18,19,20,21,22,23,24,17,26,27,28,29,30,31,32};
inline constexpr std::array<std::array<float,3>,34> g1skel34_offsets{{
{0,0,0},{.064452F,-.1027F,0},{.052F,-.030465F,0},{0,-.12412F,.025001F},
{.0021489F,-.17734F,-.078273F},{-.000094445F,-.30001F,0},{0,-.017558F,0},{0,-.035F,.14F},
{-.064452F,-.1027F,0},{-.052F,-.030465F,0},{0,-.12412F,.025001F},{-.0021489F,-.17734F,-.078273F},
{.000094445F,-.30001F,0},{0,-.017558F,0},{0,-.035F,.14F},{0,0,0},{0,.044F,-.0039635F},
{0,0,0},{.10022F,.24778F,.0039563F},{.038F,-.013831F,0},{.00624F,-.1032F,0},
{0,-.080518F,.015783F},{.00188791F,-.01F,.1F},{0,0,.038F},{0,0,.046F},{0,0,.1F},
{-.10021F,.24778F,.0039563F},{-.038F,-.013831F,0},{-.00624F,-.1032F,0},
{0,-.080518F,.015783F},{-.00188791F,-.01F,.1F},{0,0,.038F},{0,0,.046F},{0,0,.1F}}};
inline constexpr skeleton_spec smplx22_spec{"smplx22", smplx22_names, smplx22_parents, smplx22_offsets, {2,1}, {7,8,20,21}};
inline constexpr skeleton_spec soma30_spec{"soma30", soma30_names, soma30_parents, soma30_offsets, {26,22}, {24,28,13,19}};
inline constexpr skeleton_spec g1skel34_spec{"g1skel34", g1skel34_names, g1skel34_parents, g1skel34_offsets, {8,1}, {6,13,24,32}};
inline constexpr const skeleton_spec *find_skeleton(std::string_view key) noexcept {
if (key == smplx22_spec.key) return &smplx22_spec;
if (key == soma30_spec.key) return &soma30_spec;
if (key == g1skel34_spec.key) return &g1skel34_spec;
return nullptr;
}
} // namespace kimodo::detail

View File

@ -1,4 +1,5 @@
#include <kimodo/kimodo.hpp> #include <kimodo/kimodo.hpp>
#include <array> #include <array>
#include <cstdio> #include <cstdio>
int main(int argc,char**argv){if(argc!=2)return 2;auto m=kimodo::model::load(argv[1]);if(!m){std::fprintf(stderr,"%s\n",m.error().c_str());return 1;}std::array<float,kimodo::embedding_width> e{};auto r=(*m)->generate_embedding(e,2,1,42,2.f,2.f);if(!r){std::fprintf(stderr,"%s\n",r.error().c_str());return 1;}if(r->frames!=2||r->joints!=22||r->root_positions.size()!=6||r->local_rotations_xyzw.size()!=176)return 1;return 0;} #include <fstream>
int main(int argc,char**argv){if(argc<2||argc>4)return 2;const unsigned joints=argc>=3?static_cast<unsigned>(std::stoul(argv[2])):22;auto m=kimodo::model::load(argv[1]);if(!m){std::fprintf(stderr,"%s\n",m.error().c_str());return 1;}std::array<float,kimodo::embedding_width> e{};auto r=(*m)->generate_embedding(e,2,1,42,2.f,2.f);if(!r){std::fprintf(stderr,"%s\n",r.error().c_str());return 1;}if(r->frames!=2||r->joints!=joints||r->root_positions.size()!=6||r->local_rotations_xyzw.size()!=2*joints*4)return 1;if(argc==4){std::ofstream out(argv[3],std::ios::binary);out.write(reinterpret_cast<const char*>(r->root_positions.data()),static_cast<std::streamsize>(r->root_positions.size()*sizeof(float)));out.write(reinterpret_cast<const char*>(r->local_rotations_xyzw.data()),static_cast<std::streamsize>(r->local_rotations_xyzw.size()*sizeof(float)));if(!out)return 1;}return 0;}