VMC output
Prefix: /api/vmc
N.E.K.O. can publish the active VRM avatar's humanoid bones and expressions to a VMC-compatible receiver over OSC/UDP. The sender starts disabled, defaults to 127.0.0.1:39539 at 60 Hz, and only produces frames while a VRM model is active.
The preferred first-party integration is window.vrmVmcSender. The raw REST and WebSocket contracts below are implementation-facing and may evolve with the VRM runtime.
The browser initially loads only a lightweight API facade. Until a control method such as enable() or syncStatusFromBackend() is called, it does not load the full sender, poll status, create VMC timers, enter the per-frame sampling path, or change the existing VRM frame limiter.
Quick start
- Start a VMC receiver such as VSeeFace, Warudo, or a Unity/Unreal VMC integration.
- Configure the receiver to listen on UDP port
39539. - Load a VRM character in N.E.K.O.
- Enable output from the main page:
await window.vrmVmcSender.enable('127.0.0.1', 39539, 60)Optional controls:
await window.vrmVmcSender.requestTPose(2)
await window.vrmVmcSender.disable()The endpoint and send rate are persisted, but output intentionally starts disabled after a backend restart.
Output contract
The backend converts Three.js right-handed transforms to Unity/VMC coordinates and emits:
/VMC/Ext/OK/VMC/Ext/T/VMC/Ext/Root/Pos/VMC/Ext/Bone/Pos/VMC/Ext/Blend/Val/VMC/Ext/Blend/Apply
The webpage's display position, scale, and rotation are not used as the VMC root. VMC owns an independent identity root so dragging or resizing the desktop avatar does not move the receiver's world origin.
When output is disabled, the destination changes, or the active VRM is released, N.E.K.O. sends zero values for active expressions followed by /VMC/Ext/OK 0. Model-release frames are acknowledged before the browser closes its dedicated socket.
An unexpected browser publisher disconnect has a 2-second grace period. A replacement publisher that authenticates during that window continues without a terminal transition; otherwise the backend clears active expressions and sends /VMC/Ext/OK 0.
REST control plane
Mutation routes require N.E.K.O.'s same-origin CSRF headers. First-party code should call window.vrmVmcSender instead of constructing those headers manually.
GET /api/vmc/status
Returns the effective runtime state:
{
"success": true,
"enabled": false,
"host": "127.0.0.1",
"port": 39539,
"send_rate_hz": 60,
"config_path": ".../vmc_config.json",
"t_pose_requested": false,
"t_pose_duration_sec": 2.0,
"t_pose_generation": 0
}POST /api/vmc/enable
All JSON fields are optional:
{
"host": "127.0.0.1",
"port": 39539,
"send_rate_hz": 60
}host accepts an ASCII hostname or IPv4 address, port must be an integer from 1..65535, and send_rate_hz must be an integer from 1..120.
POST /api/vmc/disable
Sends the terminal VMC state, closes the UDP client, and reports the disabled runtime status.
POST /api/vmc/t_pose
Requests raw-rest-pose output for the active VRM:
{
"duration_sec": 2
}The duration must be positive and finite and is capped at 10 seconds.
WebSocket data plane
/api/vmc/ws is a dedicated first-party data channel; it is not the main chat WebSocket.
The browser must:
- Connect from an allowed local HTTP(S) origin.
- Send an
authmessage containing the current CSRF token within 5 seconds. - Wait for
{"type":"ready"}. - Send sequenced
frameorreleaseenvelopes.
Only one publisher may hold the process-wide lease. The server keeps one newest pending normal frame, serializes release after any in-flight frame, and expires a publisher after 10 seconds without a valid frame. Release and expression-retirement frames use frame_ack messages so state is not discarded before OSC transmission succeeds.
Close codes:
| Code | Meaning |
|---|---|
4403 | Origin or authentication rejected |
4409 | Message exceeds 256 KiB |
4428 | Publisher idle timeout |
4429 | Another VMC publisher is active |
Security and deployment
- Do not expose the main server port
48911to an untrusted LAN or the public Internet. - Use
127.0.0.1unless the receiver intentionally runs on another trusted machine. - OSC uses UDP and has no transport-level acknowledgement; the browser acknowledgements only confirm that N.E.K.O. handed the frame to its UDP sender.
- Development installs need the locked
python-oscdependency (uv sync).
Troubleshooting
- No motion: confirm that VMC is enabled and a VRM, not Live2D/MMD/PNGTuber, is active.
- No receiver data: verify the destination host/port, receiver listen port, and local firewall.
- Unexpected send rate: while the full-rate render loop is active, cumulative scheduling averages approximately the configured rate. A VRM that is otherwise idle is intentionally capped at about 30 Hz until animation or interaction resumes.
- Publisher busy: close the other N.E.K.O. page or wait for its 10-second lease timeout.
- Sampling error: sampling is suspended to protect rendering and retried after a later backend status poll.
