I gave my local LLM agent a face — an avatar frontend with an "emotion-beat" output contract (open-source)
I gave my local LLM agent a face — an avatar frontend with an "emotion-beat" output contract (open-source)
I got tired of keeping a Telegram tab open all day just to talk to my local agent, so I built a monitor-resident avatar that fronts it instead — a video-call-style face that performs the replies instead of me reading a wall of text.
A few things that might interest this crowd:
No runtime GPU for the avatar. Instead of live-animating a face, the agent's reply carries inline emotion-beat tags — [working], [confirm] deploy?, [happy] — and a player selects and blends between ~30 pre-rendered clips. The GPU stays 100% on the model. Code and logs render as chat cards instead of being read aloud, and [confirm] pops a human-in-the-loop approve/cancel that blocks the agent until you answer.
It hooks into a real agent as a connector. The agent's gateway dials out to a local WebSocket the app hosts, so from the agent's side the avatar is just "another channel," indistinguishable from a messenger. Adapters for Hermes and OpenClaw are included, plus a demo mode that runs with zero setup.
Local voice both ways. Edge TTS out (swappable to Qwen3-TTS / MeloTTS / Piper), Silero VAD + faster-whisper in.
Open-core, MIT. The engine is fully usable on its own: drop in a folder of clips named by emotion, point it at your agent. Avatars are pure-data bundles — no code runs when you install one — so you can build your own. The repo ships the engine only; there's no bundled character.
The engine just plays video clips — it doesn't care what produced the pixels. So the avatar can be photoreal, 2D anime, a 3D render, pixel art, anything. You can even use a Live2D or VRM model: pre-render its expressions into clips and it becomes a Ghost Vessel preset. A Live2D shell can't do the reverse — it can't take live-action footage. Clips are a superset. The trade-off is that you give up real-time rig interactivity (no dragging the head around) and you pay a one-time render step.
Repo + 45s demo: https://github.com/ghdtjrtka/ghost-vessel
The part I'd most like feedback on is the emotion-beat output format — the contract that turns model text into a UI performance. Has anyone here tried similar output contracts to drive an interface from LLM output? Curious what broke on your setup, and whether smaller local models keep the tags straight.