Skip to main content
This page explains the quickstart in steps and keeps each section copy-friendly.

Run Quickstart First

Use the clone-and-run flow first, then come back here for code details.
1

Configure environment variables

Set the required app/avatar/token values used by the frontend runtime.
.env
VITE_SPATIALREAL_AVATAR_ID=6aed28f9-674c-4ffb-89ee-b447b28aa3ed # https://app.spatialreal.ai/avatars/library
VITE_SPATIALREAL_APP_ID=your_app_id # https://app.spatialreal.ai/apps
VITE_SPATIALREAL_SESSION_TOKEN=your_temporary_session_token # https://app.spatialreal.ai/apps -> details -> Generate Temporary Token
2

Enable AvatarKit Vite plugin

The plugin is required so AvatarKit WASM assets are correctly handled in dev/build.
vite.config.ts
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import { avatarkitVitePlugin } from '@spatialwalk/avatarkit/vite'

export default defineConfig({
  plugins: [vue(), avatarkitVitePlugin()],
  server: { port: 3000 },
})
3

Implement connect + send flow in App.vue

This file handles SDK init, avatar loading, connection, and one-shot PCM send.
src/App.vue
<script setup lang="ts">
import { nextTick, onBeforeUnmount, ref } from 'vue'
import {
  AvatarManager,
  AvatarSDK,
  AvatarView,
  DrivingServiceMode,
  Environment,
} from '@spatialwalk/avatarkit'

const container = ref<HTMLDivElement | null>(null)
const status = ref('Click Connect Avatar to start')
const connecting = ref(false)
const sending = ref(false)
const connected = ref(false)

let avatarView: AvatarView | null = null

const appId = import.meta.env.VITE_SPATIALREAL_APP_ID
const avatarId = import.meta.env.VITE_SPATIALREAL_AVATAR_ID
const sessionToken = import.meta.env.VITE_SPATIALREAL_SESSION_TOKEN

if (!sessionToken) throw new Error('Missing VITE_SPATIALREAL_SESSION_TOKEN')

async function downloadPcmAudio(url: string): Promise<ArrayBuffer> {
  const response = await fetch(url)
  if (!response.ok) throw new Error('Failed to load demo audio URL')
  return response.arrayBuffer()
}

async function disposeAvatar(): Promise<void> {
  connected.value = false
  avatarView?.controller.close()
  avatarView?.dispose()
  avatarView = null
}

async function connectAvatar(): Promise<void> {
  if (connecting.value || connected.value) return
  connecting.value = true

  try {
    status.value = 'Using session token from .env...'

    if (!AvatarSDK.isInitialized) {
      await AvatarSDK.initialize(appId, {
        environment: Environment.intl,
        drivingServiceMode: DrivingServiceMode.sdk,
      })
    }
    AvatarSDK.setSessionToken(sessionToken)

    await nextTick()
    const mountEl = container.value
    if (!mountEl) throw new Error('Avatar container is not ready')

    if (!avatarView) {
      status.value = 'Loading avatar...'
      const avatar = await AvatarManager.shared.load(avatarId)
      avatarView = new AvatarView(avatar, mountEl)
      avatarView.controller.onConnectionState = (state) => {
        connected.value = state === 'connected'
      }
    }

    status.value = 'Connecting to SpatialReal...'
    await avatarView.controller.initializeAudioContext()
    await avatarView.controller.start()
    await new Promise((resolve) => setTimeout(resolve, 300))
    if (!connected.value) throw new Error('Failed to connect to animation channel')

    status.value = 'Avatar connected. Click Send Audio.'
  } catch (error) {
    status.value = error instanceof Error ? error.message : 'Failed to run demo'
  } finally {
    connecting.value = false
  }
}

async function sendAudio(): Promise<void> {
  if (sending.value) return
  if (!connected.value || !avatarView) {
    status.value = 'Please click Connect Avatar first.'
    return
  }

  sending.value = true

  try {
    status.value = 'Downloading demo PCM audio...'
    const audioData = await downloadPcmAudio('https://cdn.spatialwalk.cloud/public/website/quickstart_voice.pcm')

    status.value = 'Sending audio...'
    avatarView.controller.send(audioData, true)
    status.value = `Audio sent (${audioData.byteLength} bytes)`
  } catch (error) {
    status.value = error instanceof Error ? error.message : 'Failed to send audio'
  } finally {
    sending.value = false
  }
}

onBeforeUnmount(async () => {
  await disposeAvatar()
})
</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 || connected" @click="connectAvatar">
          {{ connecting ? 'Connecting...' : connected ? 'Avatar Connected' : 'Connect Avatar' }}
        </button>
        <button :disabled="sending || !connected" @click="sendAudio">
          {{ sending ? 'Sending...' : 'Send Audio' }}
        </button>
      </div>

      <div style="font-size:14px;">{{ status }}</div>
    </div>
  </div>
</template>
4

Understand runtime sequence

Use this as a mental model when debugging connection or audio send issues.
initialize SDK -> set session token -> load avatar -> create AvatarView -> start controller
-> fetch pcm -> controller.send(audio, true)