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 c3b048a2ab Daemon: drop the unread worker.model; tests for grace, host default, relative models root, early death
Mutation triage (StrykerJS, 81 survivors on the first full run) showed worker.model was
written and never read. The new tests pin the survivors that were behaviour: the loopback
default, --host, stray flags, the models-root resolution, the SIGTERM grace, a worker that
dies while the daemon waits for its health, the POST route, and lastError after a
requested stop.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 15:07:15 +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 Daemon: drop the unread worker.model; tests for grace, host default, relative models root, early death 2026-10-23 15:07:15 +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.