Skip to Content

🤖 Avatar Companion

Give your AI a face. A 3D companion you talk to — in a browser, in a headset, or on Vision Pro. Runs a scripted demo with no backend, and points at your own models without a rebuild.

Avatar Companion with a 3D avatar in a teal jacket in the centre, a Chat Log panel on the right reading Session: Chat-only, a message box and a row of Mic, Enter VR, Enter AR, Exit XR, Recenter, Smaller, Larger, and Settings buttons below the avatar
The main view once a session is Chat-only. The Hub install in this capture used a simulated Ollama, so no real model was involved.

Install & availability

Available now. Install from the Companion Hub App Store.

Hub App StoreSearch Spatial Companion → Install
Exposed port8123 (container serves on 5173)
Architecturelinux/amd64 and linux/arm64 (multi-arch)
Version0.2.0
Companion servicesA proxy sidecar, installed with the app, brokers chat to your Ollama

Overview

Avatar Companion renders a lip-synced 3D avatar you converse with by voice or text. It can run as a scripted demo with no backend: choose Scripted as the LLM provider and the avatar renders, animates and replies from a built-in set of responses. A Hub install starts on the Proxy provider instead, so it needs a reachable Ollama, and you confirm that with Check connection before the first answer. Connecting speech synthesis or your Companion Memory is optional, and none of it requires rebuilding the image.

To see it before you install anything, open the hosted demo at avatar.ci.computer . It’s the same app with no backend proxy, so it runs as the scripted demo (Session: Demo-only) once you close the first-run dialog.

Your conversations stay on your network. Chat is brokered to your own Ollama through a same-origin proxy, so the browser never needs CORS exceptions or a public tunnel. Crash reporting is on by default and can be turned off with APP_SHARE_CRASH_REPORTS. The reports strip credentials, file paths, request bodies, and your memories.

The app needs internet access to load. Each time the page opens, it fetches its lighting environment from a public CDN (raw.githack.com). If the device can’t reach it, the page stays blank except for an Enter XR button.

Avatars in the roster
25
Plus any GLB or VRM by URL.
from What it does today
LLM providers
5
proxy, ollama, openai, mlc, or script — set with APP_LLM_PROVIDER.
from Configuration
Facial-rig tiers detected
3
ARKit-52, Oculus visemes, or VRM expressions — whichever the model carries.
from What it does today

What it does today

  • 25 avatars in the roster, plus any URL. GLB and VRM both load. Paste a Ready Player Me .glb or any .vrm into Settings and it binds automatically.
  • Automatic facial-rig detection. ARKit-52, Oculus visemes, or VRM expressions — whichever the model actually carries. Settings shows which tier was detected and how many channels it found.
  • Lip-sync that uses real timings. Where the voice engine reports per-viseme timestamps, the mouth is driven from those directly, with coarticulation between shapes. Where it reports only word boundaries, those supply the clock and letter-to-sound rules supply the shapes.
  • Barge-in. Start talking and the avatar stops, immediately.
  • Conversation memory. Optionally recalls from and saves to Companion Memory, so it remembers across sessions. See Companion Memory.
  • Autonomous vitality. Blinking, micro-saccades, breathing, gaze, and posture that respond to whether it is listening, thinking or speaking.
  • WebXR AR + VR on Quest and other headsets, plus a visionOS build, with a browser and iPad fallback.
  • Voice input with local speech-to-text, and voice activity detection for hands-free turn-taking.

By default the avatar speaks with your browser’s built-in voices (APP_TTS_MODE=browser). Point APP_TTS_MODE at a server-backed voice for phoneme-accurate lip-sync. If that server is unreachable, the avatar still chats by text and animates normally.

Session states

The app tracks what kind of session you are in and shows it as Session: … in the Chat Log header and in the connection panel. Choose Check connection to update it, and then make the result stick: select Enable Mic in the first-run dialog, or select Save in Settings → Connection. The first-run dialog’s Skip discards the result.

StateMeaning
UnverifiedThe starting state for every provider except Scripted. The avatar answers “I am not connected yet. Open Settings and run Check connection before we talk.” until you check and apply the result. Also shown when the check finds chat unreachable, and after Skip.
Demo-onlyThe provider is Scripted. Replies come from the built-in set and use no private memory.
Chat-onlyThe check reached your model. Companion Memory isn’t in use.
Memory-readyThe check reached your model and Companion Memory.

Configuration — no rebuild required

Everything below is read from container environment at install time. An nginx entrypoint writes /config.json at boot and the app fetches it on load. The in-app Settings (⚙️) panel overrides the provider, URL, model, and voice for that browser. It doesn’t change the proxy sidecar, so APP_OLLAMA_URL (the Ollama the proxy calls) changes only in the Hub install settings.

VariablePurposeDefault
APP_LLM_PROVIDERproxy, ollama, openai, mlc, or scriptproxy
APP_PROXY_URLWhere the SPA sends chat/api/chat
APP_OLLAMA_URLOllama the proxy callshttp://host.docker.internal:11434
APP_LLM_MODELModel taggemma3:1b
APP_TTS_MODEbrowser or a server-backed voicebrowser
APP_TTS_VOICEVoice nameunset
APP_STT_REMOTESend speech input to your appliance’s Whisper instead of transcribing on the devicefalse
APP_SHARE_CRASH_REPORTSOpt out of crash reportingtrue

You don’t rebuild the image to connect a model. Set the APP_* variables above at install time, or pass them with -e when you run the container yourself.

Choosing where inference runs

Every provider except the scripted default is a different path out of the browser:

  • Proxy (recommended). The sidecar talks to your host Ollama. The browser only ever talks to the app’s own origin.
  • Ollama direct. The browser calls Ollama, so the URL must resolve from the client device and Ollama must allow the app’s origin via OLLAMA_ORIGINS.
  • OpenAI-compatible. Any endpoint speaking the OpenAI chat API.
  • On-device (WebLLM). Runs a model entirely in the browser. Needs WebGPU.
  • Scripted. No backend. This is the offline default.

Companion Memory

The proxy adds Companion Memory when its upstream is ci-server. It fetches your profile context once per session, recalls memories for each thing you say, generates the reply with your configured model, and saves the exchange afterward. If Companion Memory is unreachable, the avatar still answers, without its notes.

The Hub install form has no memory fields and doesn’t connect it for you. To turn it on, set these on the proxy sidecar (the proxy service in the app), using a memory-scoped key from Get your API keys:

VariablePurposeDefault
APP_UPSTREAMSet to ci-server to add memoryollama
APP_CISERVER_URLCompanion Memory’s API address. The proxy appends /memory/..., so include /api when you reach it through its gateway.http://host.docker.internal:3000
APP_CI_SERVER_API_KEYMemory-scoped API keyempty
APP_MEMORY_MAX_RESULTSMemories recalled per turn6
APP_MEMORY_GENERATION_UPSTREAMWhich upstream writes the replyollama

After you restart, Check connection shows Memory-ready when both your model and Companion Memory answer.

Supported platforms

PlatformModeNotes
Desktop browser (Chrome, Edge)3D / WebXRNeeds internet to load; mic needs a secure context
Meta QuestImmersive VR/ARWebXR
Apple Vision ProSpatialSeparate visionOS build
iOS / Android3D / ARMic and camera need HTTPS

Setup

Install from Hub

Search for Spatial Companion in the Hub app store and install. Hub connects it to the Ollama or OpenAI-compatible endpoint it manages when you leave Ollama URL blank.

Point it at a model

Install Ollama on your host and pull something small:

ollama pull gemma3:1b

Set Ollama URL and Model in the Hub install form if you don’t want the Hub default. The model tag has to match a model you have pulled.

Open the client

Open the app from the Local access entry on its page in the Hub dashboard, for example http://127.0.0.1:<port> on the Hub machine. See Finding Your App’s URL.

On first load the app opens a Start your companion dialog. Microphone permission and the service connection are separate. Choose Check connection first, then Enable Mic to start with voice. A reachable model changes the session from Unverified to Chat-only. If the check fails, see Troubleshooting.

Start your companion dialog with a Connection truth panel reading Session: Chat-only, Chat is reachable but voice is unavailable, Chat reachable, Memory not configured, TTS unreachable, and Skip and Enable Mic buttons
The first-run dialog after Check connection, on a Hub install with the default browser voice. The TTS line probes a server-backed voice that this setup doesn't use.

Skip closes the dialog without applying the check, so the session goes back to Unverified and the avatar refuses to answer. To use text chat without the microphone, choose Skip, open Settings (⚙️) → Connection, select Check connection, and then select Save.

Settings dialog with the Connection section showing the LLM provider set to Proxy (recommended), the Proxy URL, the Model, and a Connection truth panel reading Session: Chat-only, followed by the start of the Behavior section and Cancel and Save buttons
Settings → Connection after Check connection. Select Save to apply the result.

The connection panel can read TTS: unreachable and voice is unavailable on a default install. That line checks the proxy’s server-backed voice, which the default setup doesn’t use: the default voice mode is browser.

To try the avatar with no backend, set LLM provider to Scripted in the same dialog.

Pick an avatar

Open Settings (⚙️) → Avatar. Choose from the roster, or select Custom URL… and paste a link to any .glb or .vrm. The panel reports which facial rig was detected.

Troubleshooting

“I am not connected yet” or the session stays Unverified The app starts unverified for every provider except Scripted, and Skip in the first-run dialog leaves it that way. Open Settings → Connection, choose Check connection, and select Save. If it reports chat unreachable, confirm that Ollama is running, that the model tag exists on it, and that APP_OLLAMA_URL resolves from the app’s proxy container. The default, http://host.docker.internal:11434, works only when Ollama runs on the Hub’s host.

The page is blank, with only an “Enter XR” button The app couldn’t fetch its lighting environment from raw.githack.com. Check the device’s internet access, and any network filter that blocks that host.

LLM or TTS unreachable With the ollama provider, requests come from the browser, not the container: the URL must resolve from the client device and Ollama must allow the app’s origin via OLLAMA_ORIGINS. Switching to the proxy provider avoids both problems.

Microphone not working Mic access requires a secure context (HTTPS). Open the app over a secure hostname.

WebLLM won’t load The mlc provider needs WebGPU in the client browser.

A custom avatar loads but the mouth doesn’t move Check the facial-rig line under the URL field in Settings. A model with no morph targets is body-animation only — there are no blendshapes to drive. Ready Player Me models exported without the ARKit/Oculus morph options are the usual cause.

A custom avatar’s hair and clothing don’t move Secondary motion needs bones in the model. Many .glb avatars ship hair as a static mesh with no joints, and nothing can animate what isn’t rigged.

Last updated on