Header Logo

Building a Realtime Assistive Avatar for Voice Navigation in Swift

Building a Realtime Assistive Avatar for Voice Navigation in Swift

Build a realtime assistive avatar in Swift for voice navigation, with streamed audio, lip sync, and session lifecycle handling.

Introduction


Adding a face to a voice agent sounds cosmetic until you build one. Once a system is speaking in realtime, the timing constraints get stricter: speech has to stream, partial transcripts may change, the model can interrupt itself, and the visual layer has to stay in sync with the audio without introducing noticeable latency. If you handle that naïvely, the avatar feels “off” even when the underlying agent is correct.


This post is about building a realtime assistive avatar for voice navigation in Swift: a front end that listens, speaks, and presents a synchronized talking face while the agent helps a user move through an app, site, or product workflow. By the end, you should understand the architecture, the streaming pieces that matter, and where an avatar service fits into the stack without turning your app into a tangle of media code.


What “realtime” actually means here


For voice navigation, “realtime” is not just low latency in the abstract. It means the system can continuously accept mic input, stream intermediate ASR results, generate or relay assistant output incrementally, and render audio and video that stay perceptually aligned.


In practice, the pipeline looks like this:


  • Audio capture from the device mic.

  • Voice activity detection / turn detection so the app knows when the user has started and stopped speaking.

  • Speech-to-text or direct audio-to-agent streaming.

  • Agent response generation, often token-by-token or chunk-by-chunk.

  • Text-to-speech or streamed synthesized audio.

  • Video/avatar rendering that lip-syncs to the outgoing speech stream.


The most common mistake is to treat the avatar as a separate UI widget with its own timing. It should be downstream of the same assistant turn that produces the audio. If the avatar starts moving before audio is ready, or lags behind audio by even a few hundred milliseconds, users notice.


Architecture in Swift: keep the media path thin


In a Swift app, the cleanest design is to keep local responsibilities small:


  1. Capture microphone input and send it to the voice stack you are using.

  2. Receive assistant audio and a synchronized avatar stream.

  3. Render the avatar in a dedicated view layer.

  4. Use your app logic only for session state, permissions, and navigation actions.


That separation matters because media sync is hard to debug once business logic gets mixed in. If your SwiftUI view is also handling turn detection, transport retries, and avatar playback state, you will eventually chase heisenbugs caused by thread hops and buffering edge cases.


Swift integration patterns that hold up


There are a few practical constraints when building this in Swift:


  • Use async state boundaries. Treat session creation, token fetch, and transport setup as asynchronous operations. Don’t block the main thread waiting for media to come online.

  • Keep UI updates on the main actor. Media callbacks will usually arrive off the main thread. Convert transport events into simple UI state before touching SwiftUI or UIKit.

  • Buffer deliberately. A tiny amount of audio/video buffering is necessary for smooth playback, but too much destroys responsiveness. For voice navigation, low latency generally matters more than perfect motion smoothness.

  • Design for interruptions. Users will talk over the agent, background the app, switch routes, or lose network connectivity. Your session model should support reconnects and clean teardown.


A minimal Swift-side controller often ends up looking like an async session wrapper around a transport object, plus a view model that exposes connection status and transcript/turn state.


import Foundation

}
import Foundation

}
import Foundation

}


That example is intentionally small. The exact transport will depend on your agent stack, but the shape should stay the same: session orchestration outside the view, media rendering inside a dedicated component, and state propagated through a view model.


Lip sync is a transport problem, not just an animation problem


When people talk about avatars, they often imagine frame generation as the hard part. For voice navigation, the more important challenge is synchronization. A realistic face is not enough; it has to articulate the same speech the user hears, with no meaningful drift.


The reason this is hard is that audio and video do not progress on the same clocks. Audio is usually treated as the reference stream because humans are much more sensitive to audio timing. Video then follows the speech content and phoneme timing derived from that audio. If the system generates video from text alone, it can easily mismatch the actual spoken audio when the model edits itself mid-turn or when TTS timing changes.


So the rule of thumb is:


  • Use the final spoken audio as the source of truth for lip sync.

  • Stream the avatar only after the agent’s speech is committed enough to render.

  • Don’t let client-side animation guess at mouth movement from partial text unless you have no other choice.


For navigation use cases, this matters because the user is not watching entertainment; they are watching for confidence and clarity. A small amount of visual restraint is usually better than flashy but unstable animation.


Session lifecycle and failure modes


A useful voice-navigation avatar must behave well under the boring cases: slow networks, microphone permission denial, and session churn.


Some failure modes worth handling explicitly:


  • Session expiry. If your auth token or session lease expires mid-conversation, surface a reconnect path instead of silently freezing the avatar.

  • Mic permission changes. iOS users can revoke permissions or route audio to Bluetooth devices. Make sure your UI reflects the current audio route and recording state.

  • Turn collisions. If the user starts speaking while the assistant is still talking, decide whether you support barge-in and how the avatar transitions between speaking and listening states.

  • Backgrounding. On mobile, background transitions can pause media pipelines. Tear down cleanly and resume without leaving stale sessions around.


For navigation, barge-in is especially important. If the assistant is giving step-by-step instructions and the user says “stop” or “go back,” the system should stop speaking quickly and let the user regain control. That means your transport and session logic should be interruptible rather than strictly sequential.


Where Protoface fits


Protoface is useful here because it gives you a realtime avatar surface without making you build a custom lip-sync pipeline. For a Swift app, the typical pattern is to create or manage sessions on the server, then connect your voice agent and avatar through the service so the talking face stays synchronized with the assistant audio.


If you want to automate session creation or avatar management from your backend, the REST API is the cleanest entry point. The shape is straightforward: authenticate with an API key, create the resource you need, and hand the session information to your client.


curl -X POST https://api.protoface.com/v1/sessions \
}'
curl -X POST https://api.protoface.com/v1/sessions \
}'
curl -X POST https://api.protoface.com/v1/sessions \
}'


Exact request fields and response shapes are in the docs, but the important part is the contract: keep the secret on the server, create the realtime session there, and let the app consume only the session artifact it needs. That avoids exposing credentials in the client and keeps your Swift code focused on UX and transport integration rather than account management.


If you are already using a voice-agent framework, the LiveKit plugin path can be even thinner because it drops the avatar into the agent flow instead of forcing you to stitch together video separately. The plugin and examples live in the GitHub repo, and the broader docs cover the integration points and operational details.


A practical implementation approach


For a first version, I would recommend this order of operations:


  1. Build the voice agent and verify that barge-in, interruption, and reconnect logic work without the avatar.

  2. Add the avatar as a downstream rendering concern, not a source of state.

  3. Keep the Swift view layer dumb: it should display connection status, the video surface, and the current navigation step.

  4. Log turn boundaries and session events so you can debug timing issues later.


Once this is working, you can refine the details: avatar selection, voice style, custom navigation instructions, and recovery behavior on mobile network transitions. If you are embedding the experience on the web instead of inside an iOS app, customer-managed iframe embeds are another clean option because they avoid exposing API keys in the browser and keep the integration isolated.


Conclusion


Building a realtime assistive avatar for voice navigation is mostly an exercise in keeping media, state, and permissions well separated. The app should capture input, render output, and handle UI; the session layer should manage auth, lifecycle, and retries; the avatar layer should stay synchronized to the spoken audio.


If you keep that boundary clear, Swift remains a good fit for the client side. Start with the voice pipeline, add the avatar once the timing is stable, and use a service that handles the realtime video face so you do not have to build lip-sync infrastructure from scratch. The docs at docs.protoface.com cover the API, SDKs, and integration patterns, and the quickstarts are a good way to validate your architecture before you commit to a production implementation.

Add a face to your AI.

No credit card needed.

Add a face to your AI.

No credit card needed.

Add a face to your AI.

No credit card needed.