Swift reference

Every entry point in the Swift package: the Expression2 and Essence2Kit Swift APIs, the Essence2 C interface, credentials and return codes.

Covers the Swift package at the version on Downloads & versions. How to use these calls in an app is on iOS & iPadOS and macOS.

Products

ProductImportWhat it is
Expression2Expression2the Expression 2 engine, Swift API
Essence2KitEssence2Kitthe Essence 2 engine, Swift API; includes Essence2
Essence2Essence2 (or CLibEssence2)the Essence 2 engine, C interface
BithumanEngineProtocolBithumanEngineProtocolthe shared engine protocol. Expression2 already contains it; do not add both

All products ship ios-arm64, ios-arm64-simulator (arm64 only) and macos-arm64.

Expression 2 (Swift)

import Expression2

Expression2Credential.set(_ secret: String?)   // before create; else BITHUMAN_API_SECRET; nil clears it

static func Expression2Engine.create(
    avatarContainer: URL,             // the .imx you downloaded
    sharedEngineContainer: URL? = nil, // the shared .engine file
    sharedEngineDir: URL? = nil,
    stagingDir: URL,
    warmSpeech: [Float]? = nil) throws -> Expression2Engine

static func Expression2Engine.create(   // for containers you already unpacked
    modelPath: URL,
    sharedEngineDir: URL? = nil,
    warmSpeech: [Float]? = nil) throws -> Expression2Engine

func feed(_ samples: [Float])                   // 16 kHz mono PCM
func flushTail()                                // end of an utterance
func pull() -> (frame: [UInt8], speech: Bool)?  // BGR, width * height * 3; nil until a frame is ready
var idle: [UInt8]?                              // the next idle frame
func idle(into buffer: inout [UInt8]) -> Int    // the next idle frame into your buffer; bytes written
func resetState(clearFrames: Bool = true)       // interrupt: drop queued audio and frames
func shutdown()                                 // waits for the last usage report

// Since Swift package 2.18.0: one stream of frames to show, idle between replies, 20 fps.
func frames(audioClock: (@Sendable () -> Double?)? = nil) -> AsyncStream<Expression2Frame>  // your clock: seconds of the reply played
func nextFrame(audioClock: (@Sendable () -> Double?)? = nil) async -> Expression2Frame?     // the next frame when it is due; nil after shutdown
func events() -> AsyncStream<Expression2Event>  // .replyStarted, .replyEnded (once per reply)
func interrupt()                                // drop queued audio and frames (resetState)
var droppedFrames: Int                          // frames skipped to keep a reply on its audio's timeline
static let framesPerSecond: Double              // 20

struct Expression2Frame {
    let bgr: [UInt8]; let width: Int; let height: Int   // B, G, R bytes, width * height * 3
    let isSpeech: Bool                                   // false: idle motion between replies
    let endsReply: Bool                                  // the first frame after the reply's audio ran out
    let index: Int
    let audioTime: Double?                               // seconds into the reply's audio; 0 = its first frame, start the audio
}
var isReady: Bool
var hasPendingTail: Bool
var queuedFrames: Int
var meteringRefusal: String?
let width: Int, height: Int

static func Expression2Download.avatar(   // download an avatar file; sha256-checked, cached
    agentCode: String,
    directory: URL? = nil) async throws -> URL   // nil: Caches/bitHuman/expression2/avatars

create throws Expression2LoadError.meteringRefused(reason:) when the API secret is missing or rejected, or when the service cannot be reached at the start; meteringRefusal carries the message. pull() never blocks; poll it. Expression2Download.avatar throws Expression2Download.Failure when the download is refused, fails, or does not match its sha256.

Essence 2 (Swift)

import Essence2Kit

Essence2Credential.set(_ secret: String?)   // before create; else BITHUMAN_API_SECRET; nil clears it

static func Essence2Engine.create(
    identity: URL,                  // the Essence 2 .imx you downloaded
    resourcesDirectory: URL? = nil, // nil: fetch the runtime files once into Application Support
    readyTimeout: Double = 300) async throws -> Essence2Engine

func feed(_ samples: [Float])               // 16 kHz mono PCM; never blocks
func flushTail()                            // the reply's audio is complete; it ends now, not after 0.6 s of silence
func frames(following player: AVAudioPlayerNode) -> AsyncStream<Essence2Frame>  // speech frames on the player's audio
func frames(audioClock: (@Sendable () -> Double?)? = nil) -> AsyncStream<Essence2Frame>  // your clock: seconds of the reply played
                                            // every frame, 25 per second, until shutdown or cancellation
func events() -> AsyncStream<Essence2Event> // .replyStarted, .replyEnded (once per reply)
func nextFrame(audioClock:) async -> Essence2Frame?   // waits for the next frame; nil after shutdown
func pullFrame(audioClock:) -> Essence2Frame?         // the next frame if one is due; nil means keep the current one
func pull() -> (frame: [UInt8], speech: Bool)?   // the same, as bytes + speech flag
func idle(into out: inout [UInt8]) -> Int   // the next frame into your buffer; 0 means keep the current one
var pacing: Essence2Pacing                  // .realtime (default, 25 fps) or .unpaced (offline rendering)
var droppedFrames: Int                      // frames skipped to keep a reply on its audio's timeline
static let framesPerSecond: Double          // 25
func interrupt()                            // drop queued audio and frames
func shutdown()                             // flush the last usage report, release the engine
static func quiesceAll(timeoutMs: Int32 = 5000)   // at app exit
var width: Int
var height: Int
var isReady: Bool
var meteringRefusal: String?                // the service's refusal while this session is refused
var runtimeFailure: String?                 // set when the engine stopped
var pendingSamples: Int                     // fed audio the engine has not taken yet

struct Essence2Frame {
    let bgr: [UInt8]                        // B, G, R, width * height * 3
    let width: Int, height: Int
    let isSpeech: Bool                      // the mouth is driven by your audio
    let endsReply: Bool                     // the first frame after a reply (once per reply)
    let index: Int                          // frames handed out before this one
    let audioTime: Double?                  // speech frames: seconds into the reply's audio (0, 0.04, …)
}

Essence2Resources.ensure() async throws -> URL   // the runtime files, fetched and sha256-checked
Essence2Resources.releaseTag                     // the release they come from

static func Essence2Download.identity(   // download an avatar file; sha256-checked, cached
    agentCode: String,
    directory: URL? = nil) async throws -> URL   // nil: Caches/bitHuman/essence2/avatars

Every way of taking frames (frames, nextFrame, pullFrame, pull, idle(into:)) draws from the same engine and hands out at most 25 frames a second. A reply’s first speech frame anchors its timeline: frame k is due k/25 s later, and a frame that would be shown a full frame late is skipped, so a reply never drifts from its audio.

create throws Essence2KitError.meteringRefused(reason:) when the API secret is missing or rejected, or when the service cannot be reached at the start. It throws .identityUnreadable for a file the engine cannot open, .resourcesUnavailable when the runtime files cannot be fetched or fail their checksum, and .notReady after readyTimeout. Essence2Download.identity throws .resourcesUnavailable when the download is refused, fails, or does not match its sha256.

Essence 2 (C)

Audio is 16 kHz mono int16. Frames are packed height * width * 3 bytes in B, G, R order. Nothing blocks except be_essence2_quiesce_all.

The C library does not download its runtime files. Before be_essence2_create, put w2v_ess_fp16_v1.onnx, audio_encoder_fp16_window_trunk.onnx and audio_encoder_fp16_window_head.onnx from the essence2-v1.15.0 release at the root of your app bundle (in Xcode, add them as a group, not a folder reference), or next to the .imx. Without them the engine never becomes ready. In Swift, Essence2Kit fetches and checks these files for you.

A minimal loop

be_essence2_handle h;
be_essence2_set_api_secret(secret);                    // or Essence2Credential.set in Swift
if (be_essence2_create(imx_path, NULL, 0, &h) != 0) { /* -3: no secret, rejected, or no network */ }
while (!be_essence2_is_ready(h)) { /* show be_essence2_idle_frame() meanwhile */ }
int32_t w, hgt; be_essence2_get_info(h, &w, &hgt);     // frame is w * hgt * 3 BGR bytes
int32_t fed = 0, spoke = 0;
for (;;) {                                             // once per display tick, 25 a second
    while (fed < count) {                              // push 0.2 s at a time
        int32_t n = count - fed < 3200 ? count - fed : 3200;
        if (be_essence2_push_audio(h, pcm16k_int16 + fed, n) != 0) break;   // -2: ring full, retry next tick
        fed += n;
        if (fed == count) be_essence2_end_utterance(h);   // the reply's audio is complete
    }
    int32_t got = be_essence2_pull_frame(h, buf, w * hgt * 3);   // bytes; 0 none yet; -3 refused
    if (got < 0) break;
    if (got > 0) {
        show(buf, got);
        int32_t kind = be_essence2_last_frame_kind(h);
        if (kind == BE_ESSENCE2_FRAME_SPEECH) spoke = 1;
        else if (spoke && kind == BE_ESSENCE2_FRAME_IDLE) break;   // idle after speech: the reply is over
    }
    usleep(40000);
}
be_essence2_destroy(h);

Credentials

FunctionPurpose
int32_t be_essence2_set_api_secret(const char* secret)Sets the API secret for later sessions (NULL clears it). Without it the engine reads BITHUMAN_API_SECRET. Swift: Essence2Credential.set(_:)

Lifecycle

FunctionPurposeReturns
be_essence2_create(const char* imx_path, const char* motion_dir, int32_t chunk, be_essence2_handle* out)Opens an avatar. Pass NULL for motion_dir and 0 for chunk0; -1 bad argument; -2 the file could not be opened; -3 the session was refused (no secret, rejected secret, or no network at the start)
be_essence2_is_ready(h)Warm-up finished; speech frames can flow1 or 0
be_essence2_destroy(h)Releases one engine; background work drains on its own—
be_essence2_quiesce_all(int32_t timeout_ms)Stops every engine and waits for GPU work. Call from applicationWillTerminateengines stopped
be_essence2_last_refusal(char* out, int32_t capacity)The sentence behind the latest -3 (no secret, rejected secret, or cannot reach bitHuman)its full byte length; 0 if nothing was refused

Audio and frames

FunctionPurposeReturns
be_essence2_push_audio(h, const int16_t* samples, int32_t count)Queues speech0; -2 nothing queued (pull frames, then push again)
be_essence2_frames_available(h)Frames ready to pullcount
be_essence2_pull_frame(h, uint8_t* out, int32_t capacity)Takes the oldest framebytes written; 0 none ready; -3 the session was refused (destroy the engine)
be_essence2_get_info(h, int32_t* width, int32_t* height)Frame size for the current mode—
be_essence2_idle_frame(h, uint8_t* out, int32_t capacity)Next idle framebytes written; 0 keep the current frame; -3 refused
be_essence2_reset(h)Interrupt: drops queued audio and frames—
be_essence2_end_utterance(h)The reply’s audio is complete: the rest renders and eases to rest now, not after 0.6 s without audio0; -1 bad handle
be_essence2_last_frame_kind(h)What the last pulled frame shows: BE_ESSENCE2_FRAME_IDLE (0), _SPEECH (1) or _RAMP (2, easing back to rest). A reply is over when an idle frame follows its speechkind; -1 before any frame

Display and status

FunctionPurposeReturns
be_essence2_set_mode(h, int32_t mode)BE_ESSENCE2_MODE_FULL (0, default) or BE_ESSENCE2_MODE_HEAD (1, the square head frame)0; -1 bad handle; -2 bad mode
be_essence2_render_status(h, char* reason, int32_t len, int64_t* failures)Whether the engine’s runtime failed0 healthy; -1 failed, with reason
be_essence2_pulled_speech_frames(h)Speech frames returned so far (tells speech from idle)count
be_essence2_set_playout_anchor(h, int32_t on)Start each reply at the live audio position. Leave off if your app holds audio until the first frame0; -1 bad handle
be_essence2_reanchor_slots(h), be_essence2_anchor_slots(h)Counters for frames skipped to keep sync under loadcount

Sessions and billing

A session checks the API secret when it starts and reports its session time, talking or idle. If the network drops after the secret is accepted, frames continue for 5 minutes of rendered video, then pull_frame and idle_frame return -3 (Essence 2) or pull() returns nil (Expression 2) until the connection returns. A secret rejected mid-session is final: destroy the engine. Prices are on pricing.