# 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. It is built to run on Yggdrasil as an Idunn target (planned, not live; see Front Door). ## 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 test`), `scripts/idunn-entry.mjs` (the process Idunn runs), `deployment/idunn/recipe.toml` (the sealed release), `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 for development and tests: `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:** the Idunn target `vili` on Yggdrasil at `http://10.77.0.1:8824` over WireGuard, with its provisioning runbook in `gamecult-ops`; neither exists until the first `idunn up` lands. FBX export and a text-prompt-to-Unity-clip path with no hand steps (`docs/motion-pipeline-target.md`). ## 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: ```text 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, on CPU ``` Setup: `git submodule update --init` (CultLib, Kimodo, SOMA-X); in `vendor/CultLib`, `npm ci --ignore-scripts` and `npm run build --workspace=packages/cultcache-ts` and `--workspace=packages/cultnet-ts`; then `npm ci --install-links` here. The worker needs a Python environment with Kimodo's pins and Kimodo itself importable, and a Hugging Face cache (`/hub`) holding the Llama 3 base, both LLM2Vec adapters and the Kimodo checkpoint. Everything runs offline. ```text node scripts/vili-daemon.mjs --python --models-root --kimodo-commit --python-env [--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 `/logs/worker.log`. The daemon owns the whole worker process group: it is killed when the worker exits for any reason, on SIGTERM or SIGINT, and, for a hand-run daemon, at start-up if a crashed predecessor left one (`/worker.pid`). Under Idunn the unit's cgroup owns the tree. A hand-run daemon holds its state root through a listening socket in Linux's abstract namespace, named by the root's real path: the bind is the atomic claim, so a second daemon on a live root prints that it is held and exits 2, killing nothing, and a dead daemon's claim is gone with it (there is no lock file). Two daemons in separate network namespaces do not exclude each other. ### On Yggdrasil Idunn runs `node vili/scripts/idunn-entry.mjs --state-root --kimodo-commit ` from a sealed release of three trees: `node`, `vili` (the daemon, worker, child, `node_modules`, cultcache-py) and `python` (python-build-standalone CPython 3.12 with the hash-locked packages, Kimodo, MotionCorrection and SOMA-X installed into it; no venv). The weights are the read-only cache `/srv/vili/models`; state is `/var/lib/gamecult/vili`. Idunn's process write lease is the one writer authority: until the daemon holds it, `POST /motion/generate` answers `503 write-lease-pending` and nothing is written; if the lease is lost the worker tree is killed and it answers `503 write-lease-lost`. Idunn proves readiness with its own `POST /cultnet/snapshot` challenge, answered with a signed runtime presence (an `Origin` is still refused). `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/.json`, artifacts `.vili/artifacts//`. Routes: `GET /health`, `GET /operator-state`, `POST /motion/generate`; under Idunn also `POST /cultnet/snapshot`, Idunn's route challenge. ## 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: ```json { "prompt": "a person gives a small friendly wave", "duration": 1, "diffusionSteps": 20, "bvh": true } ``` Batch requests can use Kimodo-style metadata: ```json { "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, `/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 and prompt form, 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 the Llama 3 base with the MNTP adapter merged, then the supervised adapter, in bfloat16 on CPU, and `kimodo-text-encode.py` wraps every prompt in Llama 3 Instruct's chat header itself (`text_encoding.chat_header`, pooled after LLM2Vec's query marker), as Kimodo's LLM2Vec does when loaded from the repo ids. The identity names that prompt form. A fresh child vector matches Kimodo's own encoder from the repo ids (`test_text_encoding.ReferenceVector`, run on Yggdrasil). The measured timings are in `docs/research/vili-on-yggdrasil-cpu-2026-10.md`. 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.