No description
  • JavaScript 67.2%
  • Python 31%
  • Shell 1.8%
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
Meta Vens ffa2c7bf58 Encoder B loads from snapshot directories, as the reference vector did
Measured on Yggdrasil: loading by repo id leaves the model's _name_or_path
equal to the Llama repo id, so prepare_for_tokenization wraps the text in
the chat header and the vector matches assembly A (cosine 1.0, 0.909 to the
reference B). Loading from the snapshot paths reproduces the reference.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 12:22:25 +02:00
docs Map: Vili moves to Yggdrasil (CPU body, deletion line on Raven, cut sequence) 2026-10-23 12:04:27 +02:00
scripts Encoder B loads from snapshot directories, as the reference vector did 2026-10-23 12:22:25 +02:00
vendor Daemon: supervise the worker child, drop WSL, Docker, Eve and CORS 2026-10-23 12:14:10 +02:00
.gitignore Initial Vili animation daemon 2026-06-12 12:42:33 +01:00
.gitmodules Encoder: assembly B on CPU with a complete identity and a thread cap 2026-10-23 12:14:09 +02:00
package-lock.json Daemon: supervise the worker child, drop WSL, Docker, Eve and CORS 2026-10-23 12:14:10 +02:00
package.json Daemon: supervise the worker child, drop WSL, Docker, Eve and CORS 2026-10-23 12:14:10 +02:00
README.md README: the CPU process tree, its flags and the encoder identity 2026-10-23 12:14:10 +02:00

Vili

Vili is the GameCult Persona animation daemon.

It owns the bridge from Persona performance intent to body-motion generation. Its backend is Kimodo, run by Vili's own daemon as a CPU-only Python process tree on Linux. Admission to Idunn on Yggdrasil is the next cut; until then nothing supervises Vili.

Front Door

  • Value: a supervised path from a text prompt to generated body motion. The daemon spawns one resident Kimodo worker, caps its threads, and returns motion artifacts (motion.npz, optional BVH) for a job.
  • Problem: Personas and games need generated motion, but cold per-request model loads cost minutes. One daemon owns job identity, worker lifecycle, the thread budget and artifact references, so callers touch no model state.
  • Entry points: scripts/vili-daemon.mjs (the daemon; npm start, npm run health, npm test), scripts/kimodo-resident-worker.py (the worker it spawns), scripts/kimodo-text-encode.py (the one-shot encoder child), POST /motion/generate (see Motion Generation), docs/verse-service-contract.md and docs/motion-pipeline-target.md.
  • Live: the process tree below, run by hand: POST /motion/generate, GET /health, GET /operator-state, and the CultCache service store. It produces motion.npz and optional BVH on the SOMA skeleton; there is no FBX export.
  • Planned: admission to Idunn on Yggdrasil with a locked virtual environment, then FBX export and a text-prompt-to-Unity-clip path with no hand steps (docs/motion-pipeline-target.md). The Raven instance still runs a retired branch until the admission cut tears it down.

Authority

  • Vili owns animation job intake, the worker process lifecycle, and generated motion artifact references.
  • Personas and Weksa may propose performance intent, gesture prompts, and constraints.
  • Kimodo owns motion synthesis.
  • Future renderers own retargeting, playback, facial performance, camera, and lip sync.
  • Odin discovers Vili; Odin does not own Vili's state.
  • Operator surfaces wait for Thing 2; today Vili publishes typed state and plain HTTP.

Local Runtime

One Linux CPU process tree:

vili-daemon.mjs          job intake, worker lifecycle, thread cap, nice 10
  kimodo-resident-worker.py   resident Kimodo model, one generation at a time
    kimodo-text-encode.py     one child per request with cache misses, assembly B on CPU

Setup: git submodule update --init; in vendor/CultLib, npm ci --ignore-scripts and npm run build --workspace=packages/cultcache-ts; then npm ci here. The worker needs a Python environment with Kimodo's pins and Kimodo itself importable, and a Hugging Face cache (<models root>/hub) holding the Llama 3 base, both LLM2Vec adapters and the Kimodo checkpoint. Everything runs offline.

node scripts/vili-daemon.mjs --python <python> --models-root <hf home> --kimodo-commit <sha> --python-env <env id> [--threads 8]

Flags, each with an environment twin: --host (VILI_DAEMON_HOST, 127.0.0.1), --port (VILI_DAEMON_PORT, 8824), --state-root (VILI_STATE_ROOT, .vili), --python (VILI_PYTHON), --worker-entry (VILI_WORKER_ENTRY, the test seam), --models-root (VILI_MODELS_ROOT), --threads (VILI_THREADS, 1 to 16, default 8), --kimodo-commit (VILI_KIMODO_COMMIT), --python-env (VILI_PYTHON_ENV, the environment's identity). A generate request without a models root, Kimodo commit and Python environment id gets 503 worker-unconfigured before anything spawns.

The daemon starts the worker on the first job, on a port the worker binds itself and reports in its ready line, and respawns it after an exit. The worker runs at nice 10 with torch limited to --threads (also OMP_NUM_THREADS and MKL_NUM_THREADS), so a shared host keeps its other services. Its log is <state>/logs/worker.log. SIGTERM stops the whole tree.

POST /motion/generate refuses any POST that carries an Origin or is not application/json with 403 cross-site; no route answers a preflight.

Vili writes daemon-owned state to .vili/vili.service.cc: operator state, the command boundary and the transport profile. Jobs are .vili/jobs/<jobId>.json, artifacts .vili/artifacts/<jobId>/.

Routes: GET /health, GET /operator-state, POST /motion/generate.

Motion Generation

POST /motion/generate exposes Kimodo's generation surface through the resident worker. Vili owns job identity, persistence, timeout handling, worker lifecycle, and the output stem; Kimodo owns synthesis and model-specific export formats.

Minimal request:

{
  "prompt": "a person gives a small friendly wave",
  "duration": 1,
  "diffusionSteps": 20,
  "bvh": true
}

Batch requests can use Kimodo-style metadata:

{
  "meta": {
    "texts": [
      "a person gives a small friendly wave",
      "a person takes one careful step"
    ],
    "durations": [0.5, 0.5],
    "num_samples": 1,
    "diffusion_steps": 20
  },
  "numTransitionFrames": 3,
  "cfgType": "regular",
  "cfgWeight": [1.5],
  "seed": 6101,
  "bvh": true,
  "bvhStandardTpose": true,
  "saveExampleDir": true
}

Supported request fields:

  • prompt / utterance plus duration / durationSeconds
  • texts plus durations, or a full meta object
  • inputFolder / input_folder for a Kimodo input folder on the worker host with meta.json and optional constraints.json
  • model; changing the requested model restarts the single resident worker
  • diffusionSteps / diffusion_steps
  • numSamples / num_samples
  • numTransitionFrames / num_transition_frames
  • constraints as an object/list, or constraints, constraintsPath, or constraints_path as a path on the worker host
  • seed
  • cfg, cfgType / cfg_type, cfgWeight / cfg_weight
  • bvh, bvhStandardTpose / bvh_standard_tpose
  • noPostprocess / no_postprocess
  • saveExampleDir / save_example_dir
  • timeoutMs, workerTimeoutMs

Vili intentionally does not accept an arbitrary Kimodo output path. Generated artifacts are written under .vili/artifacts/{jobId}/ and referenced from the job record. Kimodo-SOMA-RP-v1.1 produces NPZ and optional BVH output, Kimodo-SMPLX-RP-v1.1 also exports AMASS NPZ, and Kimodo-G1-RP-v1.1 also exports CSV when that model is resident.

Current Runtime State

Kimodo's 8B text encoder (LLM2Vec, Llama 3) never lives in the worker. The worker answers Kimodo's encoder call from a CultCache store of float32 vectors, <state>/embeddings/text-embeddings.cc (gamecult.vili.text_embedding.v0, through the cultcache-py in vendor/CultLib). A vector's key is sha256(encoderIdentity + newline + text). The identity names the three Hugging Face snapshot revisions, the encoder assembly, the device, dtype, the torch, transformers, peft and accelerate versions, the Kimodo commit and the sha256 of kimodo-text-encode.py, so a changed input misses instead of serving a stale vector. Thread count is provenance, recorded in the worker identity, not identity.

The encoder is assembly B: the Llama 3 base with the MNTP adapter merged, then the supervised adapter, in bfloat16 on CPU. Kimodo's own loader never applies the MNTP weights under the pinned transformers; the A/B comparison is in docs/research/vili-on-yggdrasil-cpu-2026-10.md, which also holds the measured timings. Kimodo normalizes the prompt before encoding (A person shrugs sadly.), and the cache keys on that text. A request's misses go to one transient child that loads the encoder, encodes, writes its answer and exits, so the OS reclaims its RAM; hits never touch it. Each job carries the encoder record (hits, misses, seconds, vector hashes) and the worker identity. The cache starts empty on a new host: vectors from another assembly or device are never copied in.