Docs index: /llms.txt · every page as Markdown: add .md

‹ Platforms

iOS & iPadOS

The Swift package renders Essence 2 and Expression 2 on iPhone and iPad.

On the devicePhysical deviceAPI secretSwift package 3.0.0

The avatar renders inside your app, with no render server. Why on the device.

Quick startOpen the sample app

Before you start

You feed 16 kHz mono speech in and take lip-synced frames out. The same package builds Mac apps.

DetailExpression 2Essence 2
Rendersany character from one portraita photoreal person from one portrait
DevicesiPhone or iPad, iOS 16 or newer; measured on iPhone 15 onlyiPhone, or an M-series iPad, iOS 26 or newer; measured on iPhone 15 only
Product.product(name: "Expression2", package: "homebrew-bithuman").product(name: "Essence2Kit", package: "homebrew-bithuman") (Swift), or .product(name: "Essence2", package: "homebrew-bithuman") (C)
Credentialan API secret, Creator plan or higheran API secret, Creator plan or higher
First-run downloadabout 370 MB (avatar and shared engine)about 250 MB (avatar and engine resources)
Worked exampleiOS Expression 2iOS Essence 2
  • Xcode 26 or newer. An Apple Developer team is needed for device builds only; the Simulator needs none.
  • A paid plan (Creator or higher): billing: How billing works; pricing.
  • A physical iPhone or iPad for device builds. Essence 2 does not run in the Simulator: use a physical device. Expression 2 does run in the Simulator.
  • Essence 1 is not available on phones or in the Swift package: use Essence 2 or Expression 2 on devices (First generation).

Install

One bitHuman SDK per iOS app, for now. The current Flutter plugin and Swift package are built on different engine versions, so an iOS app that includes both may fail to link. Use only one until their versions match.

In Xcode choose File → Add Package Dependencies… and paste https://gitlab.com/bithuman/sdk/homebrew-bithuman. In a Package.swift:

.package(url: "https://gitlab.com/bithuman/sdk/homebrew-bithuman", from: "3.0.0")
// then attach the product your target uses:
//   .product(name: "Expression2", package: "homebrew-bithuman")
//   .product(name: "Essence2Kit", package: "homebrew-bithuman")
//   .product(name: "Essence2", package: "homebrew-bithuman")

The products:

ProductImportWhat it isDeployment target
Expression2import Expression2the Expression 2 engine with a Swift APIiOS 16 · macOS 13
Essence2Kitimport Essence2Kitthe Essence 2 engine with a Swift API; it includes Essence2iOS 26 · macOS 26
Essence2import Essence2the Essence 2 engine as a C library, for C, C++ and pluginsiOS 26 · macOS 26

Every product ships ios-arm64, ios-arm64-simulator (arm64 only) and macos-arm64.

BithumanAvatar, the Swift source wrapper of the 2.x package, is not in the current package: new apps use Expression2 or Essence2Kit directly. An app that depends on BithumanAvatar keeps its 2.x pin, which still resolves to the last 2.x release with the wrapper (changelog). The Flutter plugin's BithumanAvatar is a different API and is unchanged.

The package lives in the homebrew-bithuman repository, so its package identity is homebrew-bithuman; the name is expected. The Simulator slices are arm64 only: for a generic/platform=iOS Simulator or other command-line build, set EXCLUDED_ARCHS[sdk=iphonesimulator*] = x86_64 in your target’s build settings.

Authenticate

The engines check an API secret when a session starts. Set BITHUMAN_API_SECRET in the scheme's environment, or pass it in code before you create an engine: Expression2Credential.set(secret) or Essence2Credential.set(secret) (Your API secret).

The scheme's environment is for local builds. A shipped app fetches its secret from your backend and keeps it in the Keychain (What a shipped app holds).

Run your first avatar

The quickest way to see it work is the example app: clone bithuman-examples, open the iOS Expression 2 example in Xcode, set BITHUMAN_API_SECRET in the scheme and run it on your iPhone. The code below is the core of that app.

Download the wise-pup sample avatar (agent code A23WJF0199), the shared Expression 2 engine and a 16 kHz speech clip. No account is needed for these downloads:

curl -fL -o wise-pup.imx "https://api.bithuman.ai/v1/agent/A23WJF0199/model/download?model=expression-2"
curl -fLO "https://downloads.bithuman.ai/homebrew-bithuman/expression2-engine-mac-arm64-1.0.0/mac-arm64-1.0.0.engine"
curl -fL -o speech16k.wav "https://api.bithuman.ai/v1/agent/A23WJF0199/model/download?model=expression-2&member=demo_speech_16k.wav"

Add them to your app, then render. The mac engine file is the right one for iPhone apps too. For Essence 2, download the avatar in the app with Essence2Download (Swift: Integrate into your app).

Expression 2

// excerpt: inside your app. samples is the clip as [Float], 16 kHz mono;
// show(_:_:_:) draws B, G, R bytes; the URLs point at the files above.
import Expression2
import AVFoundation

Expression2Credential.set(ProcessInfo.processInfo.environment["BITHUMAN_API_SECRET"] ?? "")
let engine = try Expression2Engine.create(
    avatarContainer: avatarURL,              // wise-pup.imx
    sharedEngineContainer: sharedEngineURL,  // mac-arm64-1.0.0.engine
    stagingDir: stagingURL)                  // any writable directory; keep it between launches

let audio = AVAudioEngine(), player = AVAudioPlayerNode()         // your app's audio output
let format = AVAudioFormat(standardFormatWithSampleRate: 16000, channels: 1)!
audio.attach(player); audio.connect(player, to: audio.mainMixerNode, format: format); try audio.start()
let reply = AVAudioPCMBuffer(pcmFormat: format, frameCapacity: AVAudioFrameCount(samples.count))!
reply.frameLength = reply.frameCapacity
samples.withUnsafeBufferPointer { reply.floatChannelData![0].update(from: $0.baseAddress!, count: samples.count) }

// seconds of the current reply your player has played (nil before it starts); frames() reads it
// off the main actor, so the player goes in a Sendable box
final class PlayedSeconds: @unchecked Sendable {
    let node: AVAudioPlayerNode
    init(_ node: AVAudioPlayerNode) { self.node = node }
    func callAsFunction() -> Double? {
        guard let t = node.lastRenderTime, let pt = node.playerTime(forNodeTime: t) else { return nil }
        return Double(pt.sampleTime) / pt.sampleRate
    }
}
let played = PlayedSeconds(player)

engine.feed(samples)   // [Float], 16 kHz mono
engine.flushTail()     // end of the reply
for await frame in engine.frames(audioClock: { played() }) {
    show(frame.bgr, frame.width, frame.height)                        // B, G, R bytes
    if frame.audioTime == 0 { player.scheduleBuffer(reply, completionHandler: nil); player.play() }   // the reply's first frame: start its audio
    if frame.endsReply { break }                                      // the reply is over; idle frames follow
}

Essence 2

// excerpt: inside your app. samples is the clip as [Float], 16 kHz mono;
// show(_:_:_:) draws B, G, R bytes.
import Essence2Kit
import AVFoundation

Essence2Credential.set(ProcessInfo.processInfo.environment["BITHUMAN_API_SECRET"] ?? "")
let imxURL = try await Essence2Download.identity(agentCode: "A52DHS2219")   // sofia-ramirez
let engine = try await Essence2Engine.create(identity: imxURL)            // waits until the engine is ready

let audio = AVAudioEngine(), player = AVAudioPlayerNode()         // your app's audio output
let format = AVAudioFormat(standardFormatWithSampleRate: 16000, channels: 1)!
audio.attach(player); audio.connect(player, to: audio.mainMixerNode, format: format); try audio.start()
let reply = AVAudioPCMBuffer(pcmFormat: format, frameCapacity: AVAudioFrameCount(samples.count))!
reply.frameLength = reply.frameCapacity
samples.withUnsafeBufferPointer { reply.floatChannelData![0].update(from: $0.baseAddress!, count: samples.count) }

engine.feed(samples)                                              // [Float], 16 kHz mono
engine.flushTail()                                                // that is the whole reply
for await frame in engine.frames(following: player) {
    show(frame.bgr, frame.width, frame.height)                    // B, G, R bytes, width * height * 3
    if frame.audioTime == 0 {                                     // the reply's first speech frame:
        player.stop(); player.scheduleBuffer(reply, completionHandler: nil); player.play()  // start its audio now
    }
    if frame.endsReply { break }                                  // the reply is over; idle frames follow
}
engine.shutdown()
Expected result
  • Frame size: Expression 2 renders at 416×720. Essence 2 renders at your character’s own video resolution (up to 1080p), with the face generated at 512×512.
  • Frame bytes: every frame is packed B, G, R bytes (width * height * 3), not RGB; for example, 1920×1080 is 6,220,800 bytes.
  • Expression 2: 416×720 frames, 20 a second: idle motion between replies, speech while a reply plays. Each speech frame is handed out when your player has played its audio. The first start prepares the engine for the device; later starts reuse the staging directory.
  • Essence 2: 25 frames a second at the avatar’s own size (1080×1920 for sofia-ramirez). frames(following: player) keeps voice and lips together however long the reply is; a frame that would be shown late is skipped. The first create downloads the engine’s three runtime files (about 112 MB), checks their sha256 and keeps them in Application Support. To ship them in your app instead, pass resourcesDirectory:.

The loop on its own, without the audio setup, as it reads in your app. show, play and played are your app’s own: show draws the B, G, R frame, play(reply) starts the reply’s audio (player.scheduleBuffer(reply, completionHandler: nil) then player.play() in the example above) and played() is the clock from PlayedSeconds. You bring the conversation.

.package(url: "https://gitlab.com/bithuman/sdk/homebrew-bithuman",
         from: "3.0.0")

let engine = try Expression2Engine.create(
    avatarContainer: avatarURL,
    sharedEngineContainer: sharedEngineURL,
    stagingDir: stagingURL)

engine.feed(samples)
engine.flushTail()
for await frame in engine.frames(audioClock: { played() }) {
    show(frame.bgr, frame.width, frame.height)
    if frame.audioTime == 0 { play(reply) }
    if frame.endsReply { break }
}

Build for the Simulator from the command line. A Simulator build is unsigned, so switch signing off. From the example’s folder:

xcodebuild -project IOSExpression2.xcodeproj -scheme IOSExpression2 -destination 'generic/platform=iOS Simulator' ARCHS=arm64 CODE_SIGNING_ALLOWED=NO build

In your own project, keep ARCHS=arm64 (or the EXCLUDED_ARCHS setting from Install) for the Simulator, and pass CODE_SIGNING_ALLOWED=NO the same way.

Give the example your API secret without Xcode. Debug builds of the iOS Expression 2 example read the secret from a file instead of the scheme, so it never appears in a command line: put it in the app’s Documents/bithuman.secret.

On a device: xcrun devicectl device copy to --device <udid> --domain-type appDataContainer --domain-identifier ai.bithuman.example.ios-expression2 --source <your secret file> --destination Documents/bithuman.secret. In the Simulator, copy the file into $(xcrun simctl get_app_container booted ai.bithuman.example.ios-expression2 data)/Documents/.

The app reads the file once at launch, passes it to Expression2Credential.set(_:) and deletes it; Release builds ignore it. Never commit the file or paste the secret into a command line; a shipped app gets its secret from your backend (your API secret).

You bring the replies. The Swift package renders the speech you give it; it does not write a reply. A character that answers needs your own conversation backend (speech to text, a language model and a voice) or a key for one: you bring the conversation.

The C interface for C, C++ and plugins (Essence2) is on the Swift reference.

The wise-pup avatar mid-sentence, as drawn by the ios-expression2 example app on an iPhone 15
Measured with Swift package 2.14.2
Capture detailsCaptured on iPhone 15 (iOS 26) · Swift package 2.14.2 · wise-pup (Expression 2) · 2026-09-23.

Performance

ConfigurationEssence 2Expression 2
iPhone 15 iPhone · Swift package
2.1× real timeiPhone 15 · Swift package 2.17.3 · measured 2026-09-27
5.5× real timeiPhone 15 · Swift package 2.18.0 · measured 2026-09-27

Times real time: seconds of avatar video rendered per second. At 1.0× or more, an avatar holds a live conversation. Select a figure for its release and date. All configurations and how we measure.