Repository navigation
v0.5.0
A minor release, and additive throughout: the wire vocabulary gains names but loses none, and every new field is optional in both directions. An orchestrator written against 0.4.1 needs no attention.
Six groups, thirty gestures
affection, playful, idol, cheer, dance and photo hold five gestures each. The table is filed by group in declaration order and that order is what a control surface draws, so each group is a file of its own under src/engine/motion/gestures and the suite pins both table sizes — a dropped or duplicated entry fails the build rather than quietly shortening a menu.
Every one of the thirty is paired with a performance of the same name and a mood, so one id means the same thing whether it is asked for as a gesture or as a performance, and the thirty arrive with a face instead of being played deadpan. All of them have a lead and a hold and no sustain, the camera poses included: pose is the only group meant to stay up until it is released, and a camera pose that outlived its line would leave the hands in shot for everything said after it.
GestureGroup and PerformanceGroup widen together with the two protocol enums a compile-time equality check holds equal to them. That is what keeps the viewer, the server and the CLI naming one set of groups, and it is the whole of the minor.
One picker instead of thirteen chip rows
Thirteen groups ran the performance section far past the height of the pane, so choosing a mood meant scrolling through every pose to reach it. PresetPicker is one selected group over a fixed result area, mounted in both the panel's and the console's performance sections.
The search crosses every group at once and matches the id and both labels, with all space-separated terms having to hit: an operator who knows the name of a preset should not have to remember which group it was filed under, and the two labels are the only way a name found in one language stays findable when the other one is on screen. The group holding the current performance sits under an inset edge, so the preset that is on can be located while a different group is shown. Pressing a group clears the search and emptying the search goes back to it, which keeps a search a detour rather than a change of place the operator then has to undo.
A line whose renderers have all left
Several renderers running the same queue is the normal case, so the page identity is carried on reports as well as on the stream and every distinct renderer that acknowledged a turn.start is recorded: one start heard three times is one line with three owners. A line finishes as interrupted once all of its named owners have been gone for the stale window, keeping the pending lines behind it — a page closed mid-sentence used to leave that line airing forever, with everything after it stuck behind a turn nobody was saying any more.
The grace is armed rather than expired when a different renderer attaches, since a page that is already connected may still be posting its first turn.start, and a preview opened during a line must not shorten another window's grace or file its live line as finished.
On the renderer's own side, a teardown no longer leaks a take or a load. StageRecorder has an explicit active-take record and a generation counter, so a start that overlaps a stop, a dispose or another start cannot leave two takes running; one shutdown is shared per take rather than per caller, so a stop that arrives while a final upload is flushing waits for it. AvatarRuntime releases a GLTF that lands after teardown through a walk over the raw object graph, since the loader owns the graph but not the lifetime of the GPU resources in it. A queue torn down with its renderer forgets its pending lines without publishing queue.dropped and reports the line on air as a terminal turn.end with interrupted: true — the server queue is authoritative and has to survive an avatar swap.
The counted page is a decision, not an observation
Standing takes an optional page-count resolver, which is handed a deck id and answers a fixed count for it, never a renderer's current page. The page a deck opens on and every page a slide resolves to are clamped against that count as well as against the floor at 1, so a run of relative turns cannot count past the end of the document: an over-counted page is invisible while it climbs, and then several turns back move nothing, because a renderer has been clamping the replayed number all along while the counter is still out beyond it. With no resolver, no deck open or no usable count, the floor at 1 remains the only bound and a Standing built with nothing folds and replays exactly as it did before.
The cloned voice is on a socket and nowhere else
--port, the loopback bind address and the port environment reading are gone from the speech sidecar, so its endpoint resolves to a socket path and to nothing else. A port is reachable by every process and every user on the machine while a socket in a 0700 directory is reachable by this user alone, and while the option existed a stray environment variable was enough to put the voice on TCP without anybody deciding to. The native shell no longer starts the bundled sidecar when the configured endpoint is a port: a port endpoint means an external synthesiser the operator supplies and starts.
The reference latents are published as one directory swap — encoded into a staging directory and moved into place only once every clip has been written, with the old directory moved aside first and put back if the second move fails. At no point does the destination hold neither a complete old set nor a complete new one. The set is taken from the clips that exist now, so a recording deleted from the reference directory stops conditioning the voice.
make stop refuses a speech path that is not a socket, or is a symlink at all, before anything is probed. A path pointing at an ordinary file used to make the target delete that file; a symlink is refused even when it resolves to a socket, because the thing lsof inspects and the thing rm removes are then not the same object.
Also in this release
- A loaded motion compiles one keyframe track per channel, so a value a keyframe leaves out is genuinely unstated instead of being taken from whichever bracketing frame happened to carry it, and a spine stated once stays constant while the arms move over four keyframes. An all-zero arm or palm direction is refused on the wire, since a direction is read for its bearing and a zero one names none.
- A refused queue edit stays on screen with the text, the reading and the performance as they were typed, and the error beside the form.
reading: nullon a queue update removes a stored reading where an omitted field still leaves it untouched, which is the difference between clearing a reading and never having had one. - The MCP tool list rebuilds on a fingerprint of the vocabulary rather than on the avatar id: custom motions reloaded for the same avatar changed the ids a caller may name while leaving the announced schema exactly as it was. A command the control server refused now returns as an error result rather than a plain success.
- A fit change repaints the page already decoded, costing a
drawImageinstead of the 30-80 ms a page takes to come back from pdf.js. - A static or BGM response that closes mid-transfer destroys the file stream it was reading; a browser that navigates away or an OBS source that is removed drops the socket without ever emitting an error.
showexits nonzero when the server refused the command, so a script driving the client can tell a command that was delivered from one that reached nothing, andControlClient.commandderives its timeout from the?waitit is sending.- The demo tour selects the avatar it narrates as, and reads
--basewithparseArgsrather than by looking the flag up in the argument list.