Rapidly integrate SpatialReal Avatar with Vue + LiveKit + end-to-end model
Using this quickstart to create a Agent demo with SpatialReal SDK and LiveKit Agents.
In a few steps, you will set up a backend that uses an end-to-end model and connects to a SpatialReal session. Then, you will create a frontend Vue app that renders the SpatialReal avatar and streams microphone audio to the agent in real time.
You can just copy each code block as-is and replace values in .env.
Or just dump this page to any LLM you like and ask it to implement this demo for you.
Add these backend files exactly as shown.Create backend/.env and paste your LiveKit, Gemini Live, and SpatialReal credentials.
Both backend processes read from this file at runtime.
backend/.env
Copy
LIVEKIT_URL=wss://your-project.livekit.cloud # https://cloud.livekit.io/LIVEKIT_API_KEY=your_api_key # LiveKit Cloud -> API KeysLIVEKIT_API_SECRET=your_api_secret # LiveKit Cloud -> API KeysGOOGLE_API_KEY=your_google_api_key # https://aistudio.google.com/apikeyE2E_GOOGLE_MODEL=gemini-2.5-flash-native-audio-preview-12-2025E2E_GOOGLE_VOICE=PuckSPATIALREAL_API_KEY=your_key # https://app.spatialreal.aiSPATIALREAL_APP_ID=your_app_id # SpatialReal Studio -> ApplicationsSPATIALREAL_AVATAR_ID=your_avatar_id # SpatialReal Studio -> Avatars
Create backend/pyproject.toml to install the Python dependencies used in this guide.
Use these versions to match the quickstart runtime behavior.
Create backend/agent.py to run the Gemini Live voice agent and start the SpatialReal avatar session.
After this worker joins the room, user microphone audio is processed in real time.
Install packages, then add these frontend files exactly as shown.Run this install command in frontend to add AvatarKit RTC and a compatible LiveKit client version.
This prepares the frontend runtime before you add config and UI code.
Copy
cd frontendpnpm installpnpm add @spatialwalk/avatarkit @spatialwalk/avatarkit-rtc[email protected]
Create frontend/.env with your public avatar IDs and token endpoint.
For production, set VITE_TOKEN_ENDPOINT to your deployed backend URL.
frontend/.env
Copy
VITE_SPATIALREAL_APP_ID=your_app_id # SpatialReal Studio -> ApplicationsVITE_SPATIALREAL_AVATAR_ID=your_avatar_id # SpatialReal Studio -> AvatarsVITE_TOKEN_ENDPOINT=http://localhost:8080/token # Your backend token server URL
Update frontend/vite.config.ts to enable AvatarKit build support.
The /token proxy routes local frontend requests to your backend token server.
Create frontend/src/App.vue with a minimal video-call layout and two controls.
This component mounts the avatar canvas, connects to LiveKit, and toggles microphone publishing.
frontend/src/App.vue
Copy
<script setup lang="ts">import { nextTick, onBeforeUnmount, ref } from 'vue'import { Room } from 'livekit-client'import { AvatarManager, AvatarSDK, AvatarView, DrivingServiceMode, Environment,} from '@spatialwalk/avatarkit'import { AvatarPlayer, LiveKitProvider } from '@spatialwalk/avatarkit-rtc'const container = ref<HTMLDivElement | null>(null)const status = ref('Click Connect to start')const connecting = ref(false)const connected = ref(false)const micOn = ref(false)let avatarView: AvatarView | null = nulllet player: AvatarPlayer | null = nulllet roomClient: Room | null = nullasync function connect(): Promise<void> { if (connecting.value || connected.value) return connecting.value = true status.value = 'Requesting token...' try { const response = await fetch(import.meta.env.VITE_TOKEN_ENDPOINT || '/token', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ room: 'voice-agent-room' }), }) if (!response.ok) throw new Error('Failed to fetch token') const { token, url, room } = await response.json() if (!AvatarSDK.isInitialized) { await AvatarSDK.initialize(import.meta.env.VITE_SPATIALREAL_APP_ID, { environment: Environment.intl, drivingServiceMode: DrivingServiceMode.host, }) } await nextTick() const mountEl = container.value if (!mountEl) throw new Error('Avatar container is not ready') await player?.disconnect().catch(() => undefined) avatarView?.dispose() const avatar = await AvatarManager.shared.load(import.meta.env.VITE_SPATIALREAL_AVATAR_ID) avatarView = new AvatarView(avatar, mountEl) player = new AvatarPlayer(new LiveKitProvider(), avatarView) status.value = 'Connecting to LiveKit...' await player.connect({ url, token, roomName: room }) roomClient = player.getNativeClient() as Room if (!roomClient) throw new Error('LiveKit room client is unavailable') await roomClient.startAudio() connected.value = true status.value = 'Connected. Click Start Mic to talk.' } catch (error) { status.value = error instanceof Error ? error.message : 'Connection failed' } finally { connecting.value = false }}async function startMic(): Promise<void> { if (!player || micOn.value) return status.value = 'Starting microphone...' try { await player.startPublishing() micOn.value = true status.value = 'Mic is on. Start speaking.' } catch (error) { status.value = error instanceof Error ? `Failed to start microphone: ${error.message}` : 'Failed to start microphone.' }}async function stopMic(): Promise<void> { if (!micOn.value) return await player?.stopPublishing().catch(() => undefined) micOn.value = false status.value = 'Mic is off.'}async function disconnect(): Promise<void> { await stopMic() await player?.disconnect().catch(() => undefined) avatarView?.dispose() player = null avatarView = null roomClient = null connected.value = false status.value = 'Disconnected'}async function toggleConnection(): Promise<void> { if (connecting.value) return if (connected.value) { await disconnect() return } await connect()}async function toggleMic(): Promise<void> { if (!connected.value || connecting.value) return if (micOn.value) { await stopMic() return } await startMic()}onBeforeUnmount(async () => { await disconnect()})</script><template> <div style="min-height:100vh; display:flex; align-items:center; justify-content:center; padding:16px;"> <div style="width:min(720px, 100%); display:flex; flex-direction:column; gap:10px;"> <div ref="container" style="width:100%; aspect-ratio:16/10; min-height:320px; border-radius:12px; overflow:hidden; border:1px solid;" /> <div style="display:flex; gap:8px; flex-wrap:wrap;"> <button :disabled="connecting" @click="toggleConnection"> {{ connecting ? 'Connecting...' : connected ? 'Disconnect' : 'Connect' }} </button> <button :disabled="!connected || connecting" @click="toggleMic"> {{ micOn ? 'Stop Mic' : 'Start Mic' }} </button> </div> <div style="font-size:14px;">{{ status }}</div> </div> </div></template>
4
Verify project structure
Before running, confirm your files match this tree.
This quick check helps catch missing or misplaced files before launch.
Run these commands in three terminals, then open http://localhost:3000, click Connect, and then click Start Mic.Start the token server first so frontend can fetch JWTs.
Copy
# Terminal 1cd spatialreal-avatar-quickstart/backenduv syncuv run token_server.py
Start the agent worker second so it can join the room when dispatched.
Copy
# Terminal 2cd spatialreal-avatar-quickstart/backenduv run agent.py dev
Start the frontend last, then open the local URL and begin speaking.
Copy
# Terminal 3cd spatialreal-avatar-quickstart/frontendpnpm dev