Initial Vili animation daemon

This commit is contained in:
Meta Vens 2026-06-12 12:42:33 +01:00
commit ca6a172cdd
11 changed files with 656 additions and 0 deletions

3
.gitignore vendored Normal file
View file

@ -0,0 +1,3 @@
node_modules/
.vili/
*.log

50
README.md Normal file
View file

@ -0,0 +1,50 @@
# Vili
Vili is the GameCult Persona animation daemon.
It owns the bridge from Persona performance intent to body-motion generation. Its
first backend is Kimodo on Raven, running through Docker Engine inside
`Ubuntu-24.04` WSL with NVIDIA GPU passthrough.
## Authority
- Vili owns animation job intake, Kimodo runtime checks, generated motion
artifact references, and its operator/provider surface.
- 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.
- Idunn keeps Vili alive; Idunn does not own animation decisions.
## Local Runtime
On Raven:
```powershell
E:\Projects\Vili\scripts\restart-vili-daemon.cmd
```
Default compatibility endpoint:
```text
http://10.77.0.4:8824
```
Important routes:
- `GET /health`
- `GET /provider-advertisement`
- `GET /operator-state`
- `GET /eve/operator`
- `GET /eve/deck/providers`
- `WS /eve/deck/vili.animation`
- `GET /smoke`
- `POST /motion/generate`
## Current Blocker
Actual Kimodo generation requires a Hugging Face token with access to gated
`meta-llama/Meta-Llama-3-8B-Instruct`. Without it, Vili can verify Kimodo and
CUDA, but generation fails at model fetch.

View file

@ -0,0 +1,54 @@
# Vili Verse Service Contract
## Objective
Vili turns Persona performance intent into animation work. The near-term body is
Kimodo on Raven; the durable contract is a service surface that can accept a
gesture/action prompt, run or queue motion synthesis, and publish inspectable
state for Odin and Idunn.
## CultMesh Verses
- `vili.service`: service identity, provider advertisement, health, and runtime.
- `vili.animation`: animation jobs, prompts, backend choices, and artifact refs.
- `vili.operator`: daemon status, backend readiness, and Eve/CultUI surface.
## Witnesses
- `.vili/provider-advertisement.json`: compatibility provider advertisement.
- `.vili/operator-state.json`: daemon health and backend readiness.
- `.vili/eve-operator-surface.json`: current operator surface.
- `.vili/jobs/{jobId}.json`: accepted animation job records.
- `.vili/artifacts/{jobId}/`: generated motion outputs when generation succeeds.
JSON is currently a compatibility witness because Raven's first Vili body is a
small Node daemon. Promotion to `.cc` CultCache witnesses should happen when the
animation job schema stabilizes.
## Command Boundary
`POST /motion/generate` accepts an explicit animation request:
```json
{
"prompt": "a person gives a small friendly wave",
"duration": 2,
"diffusionSteps": 20,
"seed": 123,
"output": "wave-test",
"bvh": true
}
```
The daemon may reject or defer requests when:
- Kimodo image is unavailable.
- Docker/GPU backend is unhealthy.
- Hugging Face token is absent and the selected backend requires gated model
access.
- The request omits a concrete prompt.
## First Runtime Invariant
Health checks must be cheap. They may verify process, port, backend metadata,
and cached readiness, but they must not launch a Kimodo generation job.

36
package-lock.json generated Normal file
View file

@ -0,0 +1,36 @@
{
"name": "@gamecult/vili",
"version": "0.1.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@gamecult/vili",
"version": "0.1.0",
"dependencies": {
"ws": "^8.18.0"
}
},
"node_modules/ws": {
"version": "8.21.0",
"resolved": "https://registry.npmjs.org/ws/-/ws-8.21.0.tgz",
"integrity": "sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g==",
"license": "MIT",
"engines": {
"node": ">=10.0.0"
},
"peerDependencies": {
"bufferutil": "^4.0.1",
"utf-8-validate": ">=5.0.2"
},
"peerDependenciesMeta": {
"bufferutil": {
"optional": true
},
"utf-8-validate": {
"optional": true
}
}
}
}
}

16
package.json Normal file
View file

@ -0,0 +1,16 @@
{
"name": "@gamecult/vili",
"version": "0.1.0",
"private": true,
"type": "module",
"description": "Vili is the GameCult Persona animation daemon: speech intent in, embodied motion affordance out.",
"scripts": {
"start": "node scripts/vili-daemon.mjs",
"health": "node scripts/vili-daemon.mjs --health",
"smoke": "node scripts/smoke-vili.mjs",
"provider": "node scripts/vili-daemon.mjs --print-provider-advertisement"
},
"dependencies": {
"ws": "^8.18.0"
}
}

View file

@ -0,0 +1,2 @@
@echo off
node "%~dp0vili-daemon.mjs" --health --host 127.0.0.1 --port 8824 --state-root "%~dp0..\.vili"

View file

@ -0,0 +1,2 @@
@echo off
powershell.exe -NoProfile -ExecutionPolicy Bypass -File "%~dp0restart-vili-daemon.ps1" %*

View file

@ -0,0 +1,21 @@
param(
[int]$Port = 8824,
[string]$HostName = "0.0.0.0",
[string]$StateRoot = "E:\Projects\Vili\.vili"
)
$ErrorActionPreference = "Stop"
$PidPath = Join-Path $StateRoot "vili-daemon.pid"
if (Test-Path $PidPath) {
$existingPid = (Get-Content $PidPath -ErrorAction SilentlyContinue | Select-Object -First 1)
if ($existingPid) {
$process = Get-Process -Id ([int]$existingPid) -ErrorAction SilentlyContinue
if ($process) {
Stop-Process -Id $process.Id -Force
Start-Sleep -Seconds 1
}
}
}
& (Join-Path $PSScriptRoot "start-vili-daemon.ps1") -Port $Port -HostName $HostName -StateRoot $StateRoot

36
scripts/smoke-vili.mjs Normal file
View file

@ -0,0 +1,36 @@
#!/usr/bin/env node
const baseUrl = process.env.VILI_URL || process.argv[2] || "http://10.77.0.4:8824";
async function get(path) {
const response = await fetch(`${baseUrl}${path}`);
const text = await response.text();
let body;
try {
body = JSON.parse(text);
} catch {
body = text;
}
if (!response.ok) {
throw new Error(`${path} returned ${response.status}: ${text.slice(0, 500)}`);
}
return body;
}
const health = await get("/health");
const providers = await get("/eve/deck/providers");
const smoke = await get("/smoke");
console.log(JSON.stringify({
ok: true,
baseUrl,
health: {
service: health.service,
providerId: health.providerId,
backend: health.state?.backend,
},
providers,
smoke: {
ok: smoke.ok,
backend: smoke.backendStatus,
},
}, null, 2));

View file

@ -0,0 +1,42 @@
param(
[int]$Port = 8824,
[string]$HostName = "0.0.0.0",
[string]$StateRoot = "E:\Projects\Vili\.vili"
)
$ErrorActionPreference = "Stop"
$Root = Split-Path -Parent $PSScriptRoot
$Node = (Get-Command node -ErrorAction Stop).Source
$LogDir = Join-Path $StateRoot "logs"
$PidPath = Join-Path $StateRoot "vili-daemon.pid"
$OutLog = Join-Path $LogDir "vili-daemon.out.log"
$ErrLog = Join-Path $LogDir "vili-daemon.err.log"
New-Item -ItemType Directory -Force -Path $LogDir | Out-Null
if (Test-Path $PidPath) {
$existingPid = (Get-Content $PidPath -ErrorAction SilentlyContinue | Select-Object -First 1)
if ($existingPid -and (Get-Process -Id ([int]$existingPid) -ErrorAction SilentlyContinue)) {
& $Node (Join-Path $Root "scripts\vili-daemon.mjs") --health --host 127.0.0.1 --port $Port --state-root $StateRoot
if ($LASTEXITCODE -eq 0) {
Write-Host "Vili already healthy on port $Port with PID $existingPid"
exit 0
}
}
}
$args = @(
(Join-Path $Root "scripts\vili-daemon.mjs"),
"--host", $HostName,
"--port", "$Port",
"--state-root", $StateRoot
)
$process = Start-Process -FilePath $Node -ArgumentList $args -WorkingDirectory $Root -WindowStyle Hidden -RedirectStandardOutput $OutLog -RedirectStandardError $ErrLog -PassThru
Set-Content -Path $PidPath -Value $process.Id
Start-Sleep -Seconds 2
& $Node (Join-Path $Root "scripts\vili-daemon.mjs") --health --host 127.0.0.1 --port $Port --state-root $StateRoot
if ($LASTEXITCODE -ne 0) {
throw "Vili did not become healthy. See $ErrLog"
}
Write-Host "Vili started on $HostName`:$Port with PID $($process.Id)"

394
scripts/vili-daemon.mjs Normal file
View file

@ -0,0 +1,394 @@
#!/usr/bin/env node
import http from "node:http";
import { mkdir, readFile, writeFile } from "node:fs/promises";
import { existsSync } from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";
import { execFile } from "node:child_process";
import { WebSocketServer } from "ws";
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const projectRoot = path.resolve(__dirname, "..");
const ravenBaseUrl = process.env.VILI_PUBLIC_BASE_URL || "http://10.77.0.4:8824";
const ravenDeckUrl = process.env.VILI_PUBLIC_DECK_URL || "ws://10.77.0.4:8824/eve/deck";
const providerId = "vili.animation";
function parseArgs(argv) {
const args = {
host: process.env.VILI_DAEMON_HOST || "127.0.0.1",
port: Number(process.env.VILI_DAEMON_PORT || 8824),
stateRoot: process.env.VILI_STATE_ROOT || path.join(projectRoot, ".vili"),
health: false,
printProviderAdvertisement: false,
};
for (let index = 0; index < argv.length; index += 1) {
const arg = argv[index];
if (arg === "--host") args.host = argv[++index];
else if (arg === "--port") args.port = Number(argv[++index]);
else if (arg === "--state-root") args.stateRoot = path.resolve(argv[++index]);
else if (arg === "--health") args.health = true;
else if (arg === "--print-provider-advertisement") args.printProviderAdvertisement = true;
}
return args;
}
const args = parseArgs(process.argv.slice(2));
const startedAt = new Date().toISOString();
let backendStatus = {
checkedAt: null,
dockerImage: "unknown",
nvidiaRuntime: "unknown",
huggingFaceToken: "unknown",
kimodoHelp: "unknown",
lastError: null,
};
function run(command, commandArgs, options = {}) {
return new Promise((resolve) => {
execFile(command, commandArgs, {
cwd: projectRoot,
timeout: options.timeout ?? 20000,
env: { ...process.env, ...(options.env || {}) },
windowsHide: true,
maxBuffer: 1024 * 1024 * 4,
}, (error, stdout, stderr) => {
resolve({
ok: !error,
code: error?.code ?? 0,
signal: error?.signal ?? null,
stdout: String(stdout || "").trim(),
stderr: String(stderr || "").trim(),
error: error ? String(error.message || error) : null,
});
});
});
}
function wsl(script, options = {}) {
return run("wsl.exe", ["-d", "Ubuntu-24.04", "-u", "root", "--", "bash", "-lc", script], options);
}
function json(response, statusCode, body) {
const payload = JSON.stringify(body, null, 2);
response.writeHead(statusCode, {
"content-type": "application/json; charset=utf-8",
"cache-control": "no-store",
"access-control-allow-origin": "*",
});
response.end(payload);
}
function text(response, statusCode, body) {
response.writeHead(statusCode, {
"content-type": "text/plain; charset=utf-8",
"cache-control": "no-store",
"access-control-allow-origin": "*",
});
response.end(body);
}
function providerAdvertisement() {
return {
schema: "gamecult.eve.provider_advertisement.v1",
providerId,
title: "Vili Animation",
description: "Persona animation and Kimodo motion generation daemon on Raven.",
canonicalService: "asgard.vili",
locatedService: "asgard.raven.vili",
verseId: "raven.local",
cultMeshAddress: "asgard.raven.vili/eve/operator",
updatedAt: new Date().toISOString(),
endpoints: {
http: ravenBaseUrl,
health: `${ravenBaseUrl}/health`,
smoke: `${ravenBaseUrl}/smoke`,
providerAdvertisement: `${ravenBaseUrl}/provider-advertisement`,
operatorState: `${ravenBaseUrl}/operator-state`,
operatorSurface: `${ravenBaseUrl}/eve/operator`,
deckBroker: ravenDeckUrl,
providerDeck: `${ravenDeckUrl}/${providerId}`,
},
capabilities: [
"kimodo.motion.generate",
"persona.performance.intent",
"eve.operator.surface",
"cultmesh.provider.discovery",
],
};
}
function operatorState() {
return {
schema: "gamecult.vili.operator_state.v0",
service: "vili",
providerId,
verseId: "raven.local",
host: "raven",
startedAt,
updatedAt: new Date().toISOString(),
backend: backendStatus,
authority: {
owns: [
"animation job intake",
"Kimodo runtime checks",
"motion artifact references",
"Vili provider and operator surfaces",
],
doesNotOwn: [
"Persona speech generation",
"Weksa interlingua lowering",
"retargeting and playback",
"Odin discovery truth",
],
},
routes: {
health: "/health",
providerAdvertisement: "/provider-advertisement",
operatorState: "/operator-state",
operatorSurface: "/eve/operator",
deckProviders: "/eve/deck/providers",
smoke: "/smoke",
generate: "/motion/generate",
},
};
}
function eveSurface() {
const state = operatorState();
return {
schema: "gamecult.eve.surface.v1",
providerId,
title: "Vili Animation",
updatedAt: state.updatedAt,
surface: {
root: {
type: "panel",
title: "Vili Animation",
children: [
{ type: "stat", label: "Host", value: "Raven" },
{ type: "stat", label: "Verse", value: "raven.local" },
{ type: "stat", label: "Docker image", value: state.backend.dockerImage },
{ type: "stat", label: "NVIDIA runtime", value: state.backend.nvidiaRuntime },
{ type: "stat", label: "Kimodo", value: state.backend.kimodoHelp },
{ type: "stat", label: "HF token", value: state.backend.huggingFaceToken },
{ type: "command", label: "Smoke", method: "GET", href: `${ravenBaseUrl}/smoke` },
{ type: "command", label: "Generate motion", method: "POST", href: `${ravenBaseUrl}/motion/generate` },
],
},
},
state,
};
}
async function persistSurfaces() {
await mkdir(args.stateRoot, { recursive: true });
await writeFile(path.join(args.stateRoot, "vili-daemon.pid"), `${process.pid}\n`);
await writeFile(path.join(args.stateRoot, "provider-advertisement.json"), JSON.stringify(providerAdvertisement(), null, 2));
await writeFile(path.join(args.stateRoot, "operator-state.json"), JSON.stringify(operatorState(), null, 2));
await writeFile(path.join(args.stateRoot, "eve-operator-surface.json"), JSON.stringify(eveSurface(), null, 2));
}
async function refreshBackendStatus({ includeKimodoHelp = false } = {}) {
const docker = await wsl([
"set -e",
"service docker start >/dev/null 2>&1 || true",
"docker images gamecult/kimodo:latest --format '{{.Repository}}:{{.Tag}}' | head -n 1",
].join("; "), { timeout: 20000 });
let nvidia = await wsl([
"set -e",
"service docker start >/dev/null 2>&1 || true",
"docker info --format '{{json .Runtimes}}' | grep -q nvidia && echo configured || echo missing",
].join("; "), { timeout: 20000 });
const token = await wsl("test -f /root/.cache/huggingface/token && echo present || echo missing", { timeout: 10000 });
let help = { ok: true, stdout: backendStatus.kimodoHelp === "ok" ? "ok" : "not checked", stderr: "" };
if (includeKimodoHelp) {
nvidia = await wsl([
"set -e",
"service docker start >/dev/null 2>&1 || true",
"docker run --rm --runtime=nvidia --gpus all nvidia/cuda:12.4.1-base-ubuntu22.04 nvidia-smi --query-gpu=name --format=csv,noheader | head -n 1",
].join("; "), { timeout: 45000 });
help = await wsl([
"set -e",
"service docker start >/dev/null 2>&1 || true",
"docker run --rm --runtime=nvidia --gpus all gamecult/kimodo:latest kimodo_gen --help | sed -n '1,6p'",
].join("; "), { timeout: 60000 });
}
backendStatus = {
checkedAt: new Date().toISOString(),
dockerImage: docker.ok && docker.stdout ? docker.stdout : "missing",
nvidiaRuntime: nvidia.ok && nvidia.stdout ? nvidia.stdout : "unavailable",
huggingFaceToken: token.ok ? token.stdout : "unknown",
kimodoHelp: includeKimodoHelp ? (help.ok ? "ok" : "failed") : backendStatus.kimodoHelp,
lastError: [docker, nvidia, token, help].find((result) => !result.ok)?.stderr || null,
};
await persistSurfaces();
return { docker, nvidia, token, help, backendStatus };
}
async function readRequestBody(request) {
const chunks = [];
for await (const chunk of request) chunks.push(chunk);
const raw = Buffer.concat(chunks).toString("utf8");
return raw ? JSON.parse(raw) : {};
}
async function generateMotion(request, response) {
let body;
try {
body = await readRequestBody(request);
} catch (error) {
json(response, 400, { ok: false, error: `Invalid JSON: ${error.message}` });
return;
}
const prompt = String(body.prompt || body.utterance || "").trim();
if (!prompt) {
json(response, 400, { ok: false, error: "Missing prompt or utterance." });
return;
}
const jobId = `vili-${Date.now()}`;
const artifactDir = path.join(args.stateRoot, "artifacts", jobId);
await mkdir(artifactDir, { recursive: true });
const duration = Number(body.durationSeconds || body.duration || 4);
const linuxOutput = `/mnt/e/Projects/Vili/.vili/artifacts/${jobId}`;
const command = [
"set -e",
"service docker start >/dev/null 2>&1 || true",
`mkdir -p ${linuxOutput}`,
[
"docker run --rm --runtime=nvidia --gpus all",
"-e TEXT_ENCODER_DEVICE=cpu",
`-v /mnt/e/Projects/Vili/.vili/artifacts/${jobId}:/outputs`,
"gamecult/kimodo:latest",
"kimodo_gen",
"--output_dir /outputs",
`--duration ${duration}`,
"\"$VILI_PROMPT\"",
].join(" "),
].join("; ");
const result = await wsl(command, {
timeout: Number(body.timeoutMs || 1000 * 60 * 30),
env: { VILI_PROMPT: prompt },
});
const job = {
schema: "gamecult.vili.motion_job.v0",
jobId,
createdAt: new Date().toISOString(),
prompt,
durationSeconds: duration,
artifactDir,
ok: result.ok,
stdout: result.stdout,
stderr: result.stderr,
error: result.error,
};
await writeFile(path.join(args.stateRoot, "jobs", `${jobId}.json`), JSON.stringify(job, null, 2)).catch(async () => {
await mkdir(path.join(args.stateRoot, "jobs"), { recursive: true });
await writeFile(path.join(args.stateRoot, "jobs", `${jobId}.json`), JSON.stringify(job, null, 2));
});
json(response, result.ok ? 200 : 502, job);
}
async function handleRequest(request, response) {
const url = new URL(request.url, `http://${request.headers.host || `${args.host}:${args.port}`}`);
if (request.method === "OPTIONS") {
response.writeHead(204, {
"access-control-allow-origin": "*",
"access-control-allow-methods": "GET,POST,OPTIONS",
"access-control-allow-headers": "content-type",
});
response.end();
return;
}
if (url.pathname === "/" || url.pathname === "/health") {
await refreshBackendStatus({ includeKimodoHelp: false });
json(response, 200, { ok: true, service: "vili", providerId, state: operatorState() });
} else if (url.pathname === "/provider-advertisement") {
json(response, 200, providerAdvertisement());
} else if (url.pathname === "/operator-state") {
json(response, 200, operatorState());
} else if (url.pathname === "/eve/operator") {
json(response, 200, eveSurface());
} else if (url.pathname === "/eve/deck/providers") {
json(response, 200, {
providers: [{
id: providerId,
title: "Vili Animation",
description: "Persona animation and Kimodo motion generation daemon.",
endpoint: `${ravenDeckUrl}/${providerId}`,
}],
});
} else if (url.pathname === "/smoke") {
const result = await refreshBackendStatus({ includeKimodoHelp: true });
json(response, result.backendStatus.kimodoHelp === "ok" ? 200 : 502, { ok: result.backendStatus.kimodoHelp === "ok", ...result });
} else if (url.pathname === "/motion/generate" && request.method === "POST") {
await generateMotion(request, response);
} else {
text(response, 404, "not found\n");
}
}
async function healthCheck() {
const host = args.host === "0.0.0.0" ? "127.0.0.1" : args.host;
const response = await fetch(`http://${host}:${args.port}/health`);
if (!response.ok) throw new Error(`health returned ${response.status}`);
const body = await response.json();
if (!body.ok) throw new Error("health returned ok=false");
}
if (args.printProviderAdvertisement) {
console.log(JSON.stringify(providerAdvertisement(), null, 2));
process.exit(0);
}
if (args.health) {
healthCheck().then(() => process.exit(0), (error) => {
console.error(error.message);
process.exit(1);
});
} else {
await mkdir(path.join(args.stateRoot, "jobs"), { recursive: true });
await persistSurfaces();
if (!existsSync(path.join(args.stateRoot, "backend-status.json"))) {
await writeFile(path.join(args.stateRoot, "backend-status.json"), JSON.stringify(backendStatus, null, 2));
}
const server = http.createServer((request, response) => {
handleRequest(request, response).catch((error) => {
json(response, 500, { ok: false, error: error.message, stack: process.env.NODE_ENV === "development" ? error.stack : undefined });
});
});
const wss = new WebSocketServer({ noServer: true });
wss.on("connection", (socket) => {
const sendSurface = () => {
if (socket.readyState === socket.OPEN) socket.send(JSON.stringify(eveSurface()));
};
sendSurface();
const timer = setInterval(sendSurface, 5000);
socket.on("close", () => clearInterval(timer));
});
server.on("upgrade", (request, socket, head) => {
const url = new URL(request.url, `http://${request.headers.host || `${args.host}:${args.port}`}`);
if (url.pathname === `/eve/deck/${providerId}` || url.pathname === "/eve/deck") {
wss.handleUpgrade(request, socket, head, (ws) => wss.emit("connection", ws, request));
} else {
socket.destroy();
}
});
server.listen(args.port, args.host, () => {
console.log(`Vili listening on http://${args.host}:${args.port}`);
});
setInterval(() => persistSurfaces().catch((error) => console.error(error)), 5000);
}