iOS Essence 2

A complete SwiftUI app that renders a photoreal Essence 2 avatar at full resolution on an iPhone or iPad, on the device: four files, one setup script.

A SwiftUI app that opens an Essence 2 avatar, shows its idle motion, and speaks a line with the lips in sync, all rendered on the phone at the avatar’s own resolution (up to 1080p). Speak plays the line again.

Sources/App.swift uses Essence2Kit (Apple): Essence2Engine.create(identity:resourcesDirectory:) opens the bundled avatar, and frames(following:) paces the picture to the audio player.

Requirements

You needNotes
A Mac with Xcode 26 or newer, and an Apple Developer teama device build is a signed build
A physical iPhone, or an M-series iPad, on iOS 26the Simulator cannot run the engine; no Apple entitlement is needed
Swift package 2.18.0 or newer, Essence2Kit productthe project depends on it; a fresh clone resolves the newest release
An API secretthe engine bills session time, talking or idle
About 430 MB free on the phone and 380 MB on the Macthe avatar and the engine resources ride in the app bundle

Get the code

git clone https://github.com/bithuman-product/bithuman-examples.git
cd bithuman-examples/swift/ios-essence2
./setup.sh

setup.sh downloads the default avatar into Sources/Model/ and the engine resources into Sources/EngineResources/ (both git-ignored), checks both, and makes a speech clip. ./setup.sh <AGENT_CODE> fetches another avatar.

Set your API secret

In Xcode: Product → Scheme → Edit Scheme → Run → Environment Variables, add BITHUMAN_API_SECRET. The app reads it at launch and passes it to Essence2Credential.set(_:).

Run it

open IOSEssence2.xcodeproj

Pick your team under Signing & Capabilities, choose your iPhone as the run destination, and press Run.

Expected output

The avatar appears in its idle motion, then says the bundled line once. The console prints:

[ios-essence2] engine ready: 1080x1920, ready in <n> s

The first launch unpacks the avatar and prepares the engine, so it is slower than later launches.

How it works

  1. Credential first: Essence2Credential.set(_:) is called before the engine is created.
  2. Open the avatar: Essence2Engine.create(identity:resourcesDirectory:) opens the bundled .imx with the engine’s runtime files from Sources/EngineResources.
  3. One draw loop: frames(following: player) hands out 25 frames a second for the life of the app: idle motion between replies, and a reply’s frames as the player plays their audio.
  4. Speak: feed(_:) the reply’s 16 kHz samples, then flushTail(); the draw loop starts the reply’s audio with its first speech frame, so the lips stay on the voice.
  5. Stream, don’t collect: one 1080×1920 frame is 6.2 MB, so the app draws each frame as it arrives.

Sources/App.swift is the whole app. The full API is on Apple and Apple API reference.

The code that matters

Sources/App.swift sets the secret, opens the avatar, and draws the frames frames(following:) hands out, starting the reply’s audio with its first speech frame:

// excerpt: swift/ios-essence2/Sources/App.swift
Essence2Credential.set(ProcessInfo.processInfo.environment["BITHUMAN_API_SECRET"])
// …
let e = try await Essence2Engine.create(identity: imx, resourcesDirectory: Payload.resources)
// excerpt: swift/ios-essence2/Sources/App.swift
/// 5a. The draw loop, for the life of the app. `frames(following:)` paces itself
/// (25 per second): idle motion between replies, and a reply's frames as the player
/// plays their audio. Start the reply's audio with its first speech frame.
private func startLoop(_ e: Essence2Engine) {
    loop?.cancel()
    let frames = e.frames(following: player)
    loop = Task { [weak self] in
        for await f in frames {
            guard let self else { return }
            if f.audioTime == 0, let r = self.reply {
                self.player.stop()
                self.player.scheduleBuffer(r)
                self.player.play()
            }
            if let cg = makeCGImage(bgr: f.bgr, f.width, f.height) {
                self.sink.show(cg)
                self.hasFrame = true
            }
    // …
    e.feed(samples)
    e.flushTail()

The complete file is on GitHub.

Make it your own

  • Another sample avatar: ./setup.sh <AGENT_CODE> with any of these; the app reads the frame size back from the engine.

    AvatarAgent codeFrame
    warm-clear-professional-presenter (default)A21SKT43141080×1920
    sofia-ramirezA52DHS22191080×1920
    kwame-warm-museum-guideA62SJB39011080×1920
    afro-latina-astrophysics-mentorA23KSG52581920×1080
    calm-product-specialist-advisorA24EKJ84331280×720
  • Your own avatar: create one with the Agents API ("model": "essence-2"), then run BITHUMAN_API_SECRET=… ./setup.sh <AGENT_CODE>.

  • Ship it: fetch the secret from your backend or the Keychain at launch and pass it to Essence2Credential.set(_:); never put it in the app bundle.

Troubleshooting

SymptomFix
create throws Essence2KitError.meteringRefused(reason:)no API secret, or the service rejected it (reason says which): set BITHUMAN_API_SECRET in the Run scheme
create throws .identityUnreadablere-run ./setup.sh; it checks the download
”the shared audio front end is missing”, or the engine never becomes readyadd Sources/EngineResources as a group, not a folder reference
The avatar moves but never speaksresolve Swift package 2.18.0 or newer (File → Packages → Update to Latest Package Versions)
The link fails naming a newer minimum OSset Minimum Deployments to iOS 26.0
no such module 'Essence2Kit'attach the Essence2Kit product to the app target
ld warns “built for newer ‘iOS’ version (26.0)” once per objectexpected; the build is good
It builds for the Simulator and crashes thererun on a physical device

More on Apple: Troubleshooting.

Next