Experience (Svelte)
The canonical Svelte component for embedding Avatar Experiences.
Overview
<Experience> is the canonical Svelte API for simple embeds and
host-controlled experiences. It owns session creation, attachment, event subscriptions, restart,
and cleanup.
Basic usage
For a standard embed, pass experienceId (and optional session options). The component
mints the session, attaches the player, and runs the experience according to mode and speechInputMode. You do not need bind:this unless your app must drive speech or listening from its own UI.
<script>
import { Experience } from '@liforma/client/svelte';
<\/script>
<Experience experienceId="exp_T0I7ACMQLBMPG6K" /> Host-controlled speech and listening
“Advanced” here does not mean a separate API — it means your host app controls when the character speaks and when the microphone listens, instead of leaving that entirely to automatic conversation mode.
Typical cases:
- Scripted lessons — you call
speak()with predetermined tutor lines. - Manual capture — your Start/Stop buttons call
startListening()andstopListening(). - Custom lesson flow — turn state, feedback, and “Next” live in your page; the component still owns session lifecycle.
You still use <Experience>. Add bind:this to hold an ExperienceHandle — the typed controller for speak(), startListening(), and related methods. Type it as ExperienceHandle | undefined because the binding is undefined before mount.
What the example below does:
- Embeds the experience in presenter mode with manual speech input — the SDK does not auto-capture the learner.
- When the user taps the player-owned start button,
onStartedfires (audio unlocked) and the app speaks a welcome line viaexperience.speech.speak(). - Host Start answer / Stop answer buttons call listening methods on the handle; they stay disabled until startup completes.
For a full lesson loop (speak → Start → Stop → feedback → Next), see Guided Scripted Practice and the guided-practice SvelteKit example.
<script lang="ts">
import {
Experience,
type ExperienceHandle
} from '@liforma/client/svelte';
// Controller from bind:this — undefined until the component mounts.
let experience: ExperienceHandle | undefined = $state();
let audioUnlocked = $state(false);
// Safe point to speak: player start button has unlocked audio.
async function handleStarted() {
audioUnlocked = true;
await experience?.speech.speak({ text: 'Welcome to the lesson.' });
}
// Host-owned Start/Stop — manual speech capture for the learner.
async function startAnswer() {
if (!experience) return;
await experience.startListening();
}
async function finishAnswer() {
if (!experience) return;
const utterance = await experience.stopListening();
console.log(utterance.text);
}
<\/script>
<Experience
bind:this={experience}
experienceId="exp_T0I7ACMQLBMPG6K"
mode="presenter"
speechInputMode="manual"
onStarted={handleStarted}
/>
<button disabled={!audioUnlocked || !experience} onclick={startAnswer}>Start answer</button>
<button disabled={!audioUnlocked || !experience} onclick={finishAnswer}>Stop answer</button> Readiness
onReadyandready()mean the Session Launch is resolved and the player is attached.onStartedandstarted()mean the player start button has been used and audio is unlocked.
speak(), startListening(), stopListening(), and listenOnce() preserve the root Experience API's state validation. They wait for
component session creation if necessary, but they do not silently queue until audio is unlocked.
A call made before started rejects. Call speech methods from onStarted, await experience.started(), or keep controls disabled until
startup completes.
ExperienceProps
| Prop | Type | Description |
|---|---|---|
experienceId | string | Experience ID. SDK mints via /v1/browser-sessions (browser mint). |
launch | string | Advanced: client-fetched opaque launch. Prefer sessionEndpoint. |
sessionEndpoint | string | Same-origin route that returns SessionLaunchResponse. For server-session embeds. |
locale | string | Primary / user language (BCP 47). Defaults to navigator.language when
omitted. |
secondaryLocale | string | Dual: paired / learning language (BCP 47). Non-dual Match: immerses the session in this language. Required for dual when Studio left the paired axis as Match. |
learningLocale | string | Alias of secondaryLocale — same value works for tutors and roleplay in
language-learning apps. |
mode | ExperienceMode | Conversation or presenter behavior. |
speechInputMode | SpeechInputMode | Automatic, manual, or disabled speech capture. |
startButton | StartButtonOptions | Player-owned startup button configuration. |
theme | PlayerTheme | Global player chrome (introduction, feedback, panels, shared buttons). Presentation only —
does not remint. Use startButton for CTA-only overrides. |
showFeedback | boolean | When true (default), the player shows end-of-session Feedback step-through.
Set false to hide Liforma Feedback UI and render scores yourself via onFeedback / feedback. Presentation only — does not remint;
not a Studio setting. |
conversationProcessor | ConversationProcessorFn | Browser-owned conversation processor. |
speechOnly | boolean | Run STT, TTS, and conversation without loading the avatar renderer or location scene
assets. Same experienceId as a full embed. Default false. Bare speechOnly is equivalent to speechOnly=true. |
fit | 'full' | 'medium' | 'face' | Scene framing in the host container. Default 'full' height-fits the whole
avatar and location. 'medium' is a bust window (0.75× face-mesh height above
the oval, 2× below). 'face' frames the mesh face oval with 0.5H above and
below and at least 0.25W on each side (location background zooms with the face) — useful
for small inset / PIP layouts.
Changing the prop (or calling setFit()) updates the live player without
reminting. Orthogonal to location video-call presentation (hasZoomVariant). |
avatarId, backdropId | string | Public-session avatar or backdrop overrides. locationId remains a deprecated
alias for backdropId. |
embedBaseUrl | string | Player embed origin override, normally only for local development. |
debug | boolean | Enable SDK debug behavior. |
returnUrl | string | Close destination URL (player / attach chrome — does not remint). |
Callback props
Svelte integrations use typed Svelte 5 callback props, not manual .on() subscriptions or legacy on: component events.
<script lang="ts">
import { Experience } from '@liforma/client/svelte';
function reportError(error: Error) {
console.error(error);
}
<\/script>
<Experience
experienceId="exp_T0I7ACMQLBMPG6K"
onReady={({ session }) => console.log('Attached', session.id)}
onStarted={({ mode }) => console.log('Audio unlocked', mode)}
onUserTranscript={(update) => console.log('Transcript', update.text)}
onPlayerStatusChange={(status) => console.log('Player status', status)}
onClose={(event) => console.log('Player closed', event)}
onError={reportError}
/> | Callback | When it runs |
|---|---|
onReady | Manifest resolved and player attached. |
onStarted | User gesture completed and audio unlocked. |
onUserTranscript | Partial or final user transcript update. |
onPlayerStatusChange | Embedded player status (loading | scene | ready | error). |
onActivityChange | Session activity envelope (data: idle | listening | thinking | speaking). |
onClose | Player requests close; supplying it overrides default close navigation. |
onError | Initialization or runtime operation reports an error. |
Additional typed callbacks are onUserSpeechStarted, onUserSpeechEnded, onCharacterSpeechStarted, onCharacterSpeechEnded, onCharacterFocusChanged, onConversationUpdate, onMessage, onListeningState, and onConversationProcessorError.
ExperienceHandle
| Method | Result |
|---|---|
ready() | Promise<ReadyEvent> |
started() | Promise<StartedEvent> |
speak(options) | Promise<SpeechResult> |
startListening() | Promise<void> |
stopListening() | Promise<UtteranceResult> |
listenOnce(options?) | Promise<UtteranceResult> |
getManifest() | Current SessionManifest, or null before ready. |
getConversation() | Current immutable conversation snapshot. |
focusCharacter(characterId) | Presentation-only focus on a multi-Character node; no-op on single-Character nodes. |
setFit(fit) | Live scene framing: 'full', 'medium', or 'face'.
Does not remint. |
end() | Ends the current session. |
Reactive restart behavior
Changing developer-intent props restarts the owned session: experienceId, sessionEndpoint, launch, locale, secondaryLocale / learningLocale, mode, speechInputMode, conversationProcessor, speechOnly, avatarId, backdropId (deprecated alias locationId), embedBaseUrl, or debug. You do not need a {#key ...} block.
Presentation props (fit, startButton, theme, showFeedback, closeButton, settingsButton, theatreButton, fullscreenButton, returnUrl, ui) update the live player without reminting. Callback-only changes do not restart
the session.
Server-session embed
<Experience
experienceId="exp_T0I7ACMQLBMPG6K"
sessionEndpoint="/api/liforma-session"
/> Container sizing
The component fills its parent container. Set explicit dimensions on the parent for embedded layouts, or use full viewport for immersive experiences.
<div style="width: 400px; height: 600px;">
<Experience experienceId="exp_T0I7ACMQLBMPG6K" />
</div> For a face-sized inset (for example bottom-right PIP), size the host div and set fit="face" so the runtime zooms onto the mesh face oval instead of fitting the full
body:
<div class="avatar-inset">
<Experience experienceId="exp_T0I7ACMQLBMPG6K" fit="face" />
</div>
<style>
.avatar-inset {
position: fixed;
right: 16px;
bottom: 16px;
width: 280px;
height: 280px;
}
</style> SSR safety
Do not put opaque launch through server load functions into page
data — it would be embedded in HTML. Prefer experienceId for browser embeds or a
same-origin sessionEndpoint.
Close behavior
Without onClose, the component follows the returnUrl prop (player /
attach chrome). Supplying onClose disables automatic navigation so your app can
handle the event.
Deprecated alias
LiformaExperience remains available from @liforma/client/svelte during
the temporary deprecation window. New code should use Experience.
Using the imperative API too
If a file genuinely needs both APIs, import the framework-neutral class with an alias. Most Svelte integrations should let the component own the lifecycle.
import { Experience } from '@liforma/client/svelte';
import { Experience as ExperienceApi } from '@liforma/client';