# bitHuman — Platforms (iOS & iPadOS, macOS, Android, Flutter, Web, Python, CLI, LiveKit, REST) > Realtime talking avatars from one portrait. Index: https://docs.bithuman.ai/llms.txt · every page is also served as markdown at .md · OpenAPI: https://docs.bithuman.ai/api/openapi.yaml · other sections: https://docs.bithuman.ai/llms/start.txt · https://docs.bithuman.ai/llms/deploy.txt · https://docs.bithuman.ai/llms/models.txt · https://docs.bithuman.ai/llms/build.txt · https://docs.bithuman.ai/llms/api.txt ## Instructions for AI agents - Always send `model` ("essence-2" or "expression-2") when creating an agent; poll until `status` is `ready` or `failed`. - API and SDK use requires the Creator plan or higher. Never tell a user they can build on a free plan. - In a LiveKit worker, name the secret `BITHUMAN_MASTER_SECRET` and pass a minted token (https://docs.bithuman.ai/platforms/livekit.md). - Say where things happen: the avatar renders (device, browser, server, cloud); the conversation runs (your stack, the CLI's local conversation brain, or bitHuman's servers). - Quote speed only from https://docs.bithuman.ai/performance.json (× real time, with the device). Quote prices only from GET https://api.bithuman.ai/v1/pricing. - Do not claim offline on phones, Mac or the browser, a conversation brain on phones, or any certification. Where each model runs, the key facts and one command per path: https://docs.bithuman.ai/llms.txt ## Contents - iOS & iPadOS — https://docs.bithuman.ai/platforms/ios - macOS — https://docs.bithuman.ai/platforms/macos - Android — https://docs.bithuman.ai/platforms/android - Flutter — https://docs.bithuman.ai/platforms/flutter - Web: embed and WebGPU — https://docs.bithuman.ai/platforms/web - Python — https://docs.bithuman.ai/platforms/python - CLI — https://docs.bithuman.ai/platforms/cli - LiveKit — https://docs.bithuman.ai/platforms/livekit - REST API — https://docs.bithuman.ai/platforms/rest Linked, not inlined (read the .md twin): - Local conversation brain — https://docs.bithuman.ai/platforms/cli/local-brain.md - Examples: https://docs.bithuman.ai/examples.md · changelog: https://docs.bithuman.ai/changelog.md · API references: https://docs.bithuman.ai/platforms/cli/reference.md, https://docs.bithuman.ai/platforms/python/reference.md, https://docs.bithuman.ai/platforms/swift/reference.md, https://docs.bithuman.ai/platforms/android/reference.md --- # iOS & iPadOS URL: https://docs.bithuman.ai/platforms/ios > The Swift package renders Essence 2 and Expression 2 on iPhone and iPad. One Swift package carries both models. The avatar renders inside your app on the iPhone or iPad: you feed 16 kHz mono speech in and take lip-synced frames out, with no render server. The same package builds [Mac apps](https://docs.bithuman.ai/platforms/macos). Why render on the device: - What reaches bitHuman: When the avatar renders in your app on the device and you use your own voice and language services, bitHuman receives usage metering only, never audio, video or conversation text. - What it costs: 2 credits per minute of active session time on the device, against 4 for a bitHuman cloud avatar: about $0.02 and $0.04 a minute at the top-up rate ([pricing](https://docs.bithuman.ai/pricing)). - When the network drops: A session checks your credential when it starts and keeps rendering through a network drop of up to 5 minutes. *Capture: The wise-pup avatar mid-sentence, as drawn by the ios-expression2 example app on an iPhone 15. Captured on iPhone 15 (iOS 26) · Swift package 2.14.2 · wise-pup (Expression 2) · 2026-09-23.* (https://docs.bithuman.ai/examples/ios-expression-2/poster.webp) | Detail | Expression 2 | Essence 2 | |---|---|---| | **Renders** | [any character from one portrait](https://docs.bithuman.ai/models/expression-2) | [a photoreal person from one portrait](https://docs.bithuman.ai/models/essence-2) | | **Devices** | any Apple silicon iPhone or iPad, iOS 16 or newer | any Apple silicon iPhone, an M-series iPad, iOS 26 or newer | | **Product** | `.product(name: "Expression2", package: "homebrew-bithuman")` | `.product(name: "Essence2Kit", package: "homebrew-bithuman")` (Swift), or `.product(name: "Essence2", package: "homebrew-bithuman")` (C) | | **Credential** | an [API secret](https://docs.bithuman.ai/start/api-secret), Creator plan or higher | an API secret, Creator plan or higher | | **First-run download** | about 370 MB (avatar and shared engine) | about 250 MB (avatar and engine resources) | | **Worked example** | [iOS Expression 2](https://docs.bithuman.ai/examples/ios-expression-2) | [iOS Essence 2](https://docs.bithuman.ai/examples/ios-essence-2) | ## Before you start - **Xcode 26 or newer** and an Apple Developer team. - **A physical iPhone or iPad** for device builds. Essence 2 does not run in the Simulator; Expression 2 does. - **Essence 1** is not available on phones or in the Swift package: use Essence 2 or Expression 2 on devices ([First generation](https://docs.bithuman.ai/models/first-generation)). ## Install In Xcode choose *File → Add Package Dependencies…* and paste `https://github.com/bithuman-product/homebrew-bithuman.git`. In a `Package.swift`: ```swift .package(url: "https://github.com/bithuman-product/homebrew-bithuman.git", from: "2.19.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: | Product | Import | What it is | Deployment target | |---|---|---|---| | `Expression2` | `import Expression2` | the Expression 2 engine with a Swift API | iOS 16 · macOS 13 | | `Essence2Kit` | `import Essence2Kit` | the Essence 2 engine with a Swift API; it includes `Essence2` | iOS 26 · macOS 26 | | `Essence2` | `import Essence2` | the Essence 2 engine as a C library, for C, C++ and plugins | iOS 26 · macOS 26 | Every product ships `ios-arm64`, `ios-arm64-simulator` (arm64 only) and `macos-arm64`. `bitHumanKit` 2.4.0 is legacy and frozen; new apps use `Expression2` or `Essence2Kit`. ## 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](https://docs.bithuman.ai/start/api-secret)). The scheme's environment is for local builds. Every copy of a shipped app carries its secret, so treat it as exposed: fetch it from your backend at startup, keep it in the Keychain, give each app its own secret, and rotate it if usage looks wrong ([What a shipped app holds](https://docs.bithuman.ai/start/api-secret#what-a-shipped-app-holds)). Credits pay for active session time, talking or idle, billed to the second ([pricing](https://docs.bithuman.ai/pricing)). ## First frame Download the `wise-pup` sample avatar, the shared Expression 2 engine and a 16 kHz speech clip. No account is needed for these downloads: ```bash curl -fL -o A23WJF0199.imx "https://api.bithuman.ai/v1/agent/A23WJF0199/model/download?model=expression-2" curl -fLO "https://github.com/bithuman-product/homebrew-bithuman/releases/download/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` ([below](#download-an-avatar-in-the-app)). ```swift tab="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, // A23WJF0199.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); player.play() } // the reply's first frame: start its audio if frame.endsReply { break } // the reply is over; idle frames follow } ``` ```swift tab="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); player.play() // start its audio now } if frame.endsReply { break } // the reply is over; idle frames follow } engine.shutdown() ```
Expected result - **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 C interface for C, C++ and plugins (`Essence2`) is on the [Swift reference](https://docs.bithuman.ai/platforms/swift/reference#essence-2-c). ## Complete example Two SwiftUI apps you can clone and run on an iPhone or iPad, each with a microphone button, idle motion and interruption: - [iOS Expression 2](https://docs.bithuman.ai/examples/ios-expression-2): the `wise-pup` sample avatar. - [iOS Essence 2](https://docs.bithuman.ai/examples/ios-essence-2): a photoreal Essence 2 avatar at full resolution. ## Integrate into your app | Job | Expression 2 | Essence 2 | |---|---|---| | Audio in | 16 kHz mono `[Float]`: `feed(chunk)` as it arrives | the same | | Show frames | `frames(audioClock:)` on your player's clock, or `pull()`, which returns frames as soon as they render | `frames(following:)`, or `pull()` paced to 25 a second | | End of a reply | `flushTail()`; the first frame after it has `endsReply`, and `events()` reports `.replyEnded` | the same | | Start the reply's audio | with its first frame (`audioTime == 0`, or `events()` `.replyStarted`) | with its first speech frame; `frames(following: player)` keeps the picture on it | | Idle between replies | `frames()` keeps returning idle frames (`isSpeech == false`), or `engine.idle` | `frames()` / `pull()` keep returning idle frames | | Interrupt the reply | `interrupt()` | `interrupt()` | | Check the session | `meteringRefusal` | `meteringRefusal`, `runtimeFailure` | | Quit | `shutdown()` | `shutdown()`, then `Essence2Engine.quiesceAll()` from `applicationWillTerminate` | For file rendering, set `engine.pacing = .unpaced`: Essence 2 then hands out frames as fast as it renders them. If your audio does not go through an `AVAudioPlayerNode`, pass your own clock: `frames(audioClock: { secondsOfThisReplyPlayed })`. Resample 24 kHz speech (OpenAI Realtime's) to 16 kHz, and close the avatar when the app leaves the screen: [Companion app](https://docs.bithuman.ai/build/companion-app#resample-speech-to-16-khz). ### Download an avatar in the app Your app can download an avatar file itself, with the secret you set in [Authenticate](#authenticate): ```swift let avatarURL = try await Expression2Download.avatar(agentCode: "A23WJF0199") // Expression 2 let imxURL = try await Essence2Download.identity(agentCode: "A52DHS2219") // Essence 2 ``` Both return a local file to pass to `create`. They download the Apple build of the avatar, which is smaller than the full file, and refuse a file whose sha256 does not match. Files are kept in the app's Caches directory under their sha256, so a second call for the same avatar downloads nothing. Pass `directory:` to keep them somewhere else. The shared Expression 2 engine file is not an avatar; download it from the release as in [First frame](#first-frame). ## Platform notes - **Your own MLX:** Essence 2 contains no MLX. Link your own `mlx-swift` (`MLX`, `MLXNN`) in the same target, also with `-ObjC` or `-all_load`; nothing to embed. - **Simulator:** simulator slices are arm64 only; pass `ARCHS=arm64`. Essence 2 does not run in the Simulator (`be_essence2_create` returns `-2`); Expression 2 does. - **Privacy strings:** add `NSMicrophoneUsageDescription` to hear the user. - **Check the version you resolved.** SwiftPM keeps what `Package.resolved` holds, so run `swift package update` after you raise `from:`, then read it back: ```bash grep -A3 homebrew-bithuman Package.resolved # "version" must be the one on Downloads & versions ``` ## Performance | Configuration | Hardware | Essence 2 | Expression 2 | Measured | |---|---|---|---|---| | iPhone · Swift package | iPhone 15 | 2.1× real time | 5.5× real time | Swift package 2.17.3 and Swift package 2.18.0, 2026-09-27 | | iPhone · Swift package (held 10 min) | iPhone 15 | 1.3× real time | 5.1× real time | Swift package 2.15.0, 2026-09-25 | × real time: seconds of video rendered per second; 1.0× or more holds a live conversation ([method](https://docs.bithuman.ai/performance#mobile)). ## Troubleshooting | Symptom | Cause | Fix | |---|---|---| | `create` throws `meteringRefused` (C: `be_essence2_create` returns `-3`): *no API secret was found* | no secret | call `Essence2Credential.set` / `Expression2Credential.set`, or set `BITHUMAN_API_SECRET` in the scheme | | *the API secret was rejected (401)* | revoked or mistyped secret | create a new one under [API secrets](https://www.bithuman.ai/developer/api-keys) | | *cannot reach bitHuman to verify your credential* | no network at first contact | fix the network, then create again | | `pull()` keeps returning `nil` right after `feed()` | frames arrive asynchronously, and Essence 2 hands out at most 25 a second | poll, or use `frames()` | | crash in `__cxa_finalize` when the app quits | `Essence2Engine.quiesceAll()` (C: `be_essence2_quiesce_all`) was not called | call it from `applicationWillTerminate` | | `unable to resolve module dependency: 'Expression2'` on a Simulator build | the default destination also builds x86_64 | add `ARCHS=arm64` | | `duplicate symbol` naming `MLX` at the final link, or Essence 2 memory rising in a long session | an older Swift package | raise `from:` to the version on [Downloads & versions](https://docs.bithuman.ai/downloads), then `swift package update` | | a link error naming `BithumanEngineProtocol` | that product was added beside `Expression2`, which already contains it | depend on `Expression2` only | | `401 MISSING_AUTH` downloading a model | the agent code and `model=` do not match a sample avatar | check the code, or send your API secret for your own agent | ## Reference - [Swift reference](https://docs.bithuman.ai/platforms/swift/reference): every Swift and C entry point. - Examples: [iOS Expression 2](https://docs.bithuman.ai/examples/ios-expression-2) · [iOS Essence 2](https://docs.bithuman.ai/examples/ios-essence-2) · [macOS Expression 2](https://docs.bithuman.ai/examples/macos-expression-2). - Sample avatars: [Ready-made avatars](https://docs.bithuman.ai/examples#ready-made-avatars). Your own agent's model: [`GET /v1/agent/{code}/model/download`](https://docs.bithuman.ai/api/agents#download-an-agents-model) with your API secret. - [Changelog](https://docs.bithuman.ai/changelog) and [Downloads & versions](https://docs.bithuman.ai/downloads). --- # macOS URL: https://docs.bithuman.ai/platforms/macos > Render Essence 2 and Expression 2 on a Mac with Apple silicon, in a Mac app or from a terminal, with the same package as iPhone and iPad. The same Swift package that runs on iPhone and iPad renders the avatar on a Mac with Apple silicon: in a Mac app, or in a command-line tool with `swift run`. No device, provisioning profile or entitlement is needed to try it from a terminal. Why render on the device: - What reaches bitHuman: When the avatar renders in your app on the device and you use your own voice and language services, bitHuman receives usage metering only, never audio, video or conversation text. - What it costs: 2 credits per minute of active session time on the device, against 4 for a bitHuman cloud avatar: about $0.02 and $0.04 a minute at the top-up rate ([pricing](https://docs.bithuman.ai/pricing)). - When the network drops: A session checks your credential when it starts and keeps rendering through a network drop of up to 5 minutes. *Capture: The wise-pup avatar speaking, rendered by the macos-expression2 example. Captured on an Apple M4 iMac (macOS) · Swift package 2.14.2 · wise-pup (Expression 2) · 2026-09-23.* (https://docs.bithuman.ai/examples/macos-expression-2/clip.mp4) ## Before you start | You need | Expression 2 | Essence 2 | |---|---|---| | **A Mac** | Apple silicon, macOS 13 or newer | Apple silicon M3 or newer, macOS 26 or newer | | **Toolchain** | Xcode 26 or newer (to build) | the same | | **Credential** | an [API secret](https://docs.bithuman.ai/start/api-secret) (Creator plan or higher) | the same | | **Disk** | about 800 MB for the example: downloads plus the folder the engine unpacks them into | about 250 MB of downloads | ## Install In Xcode choose *File → Add Package Dependencies…* and paste `https://github.com/bithuman-product/homebrew-bithuman.git`. In a `Package.swift`: ```swift .package(url: "https://github.com/bithuman-product/homebrew-bithuman.git", from: "2.19.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: | Product | Import | What it is | Deployment target | |---|---|---|---| | `Expression2` | `import Expression2` | the Expression 2 engine with a Swift API | iOS 16 · macOS 13 | | `Essence2Kit` | `import Essence2Kit` | the Essence 2 engine with a Swift API; it includes `Essence2` | iOS 26 · macOS 26 | | `Essence2` | `import Essence2` | the Essence 2 engine as a C library, for C, C++ and plugins | iOS 26 · macOS 26 | Every product ships `ios-arm64`, `ios-arm64-simulator` (arm64 only) and `macos-arm64`. `bitHumanKit` 2.4.0 is legacy and frozen; new apps use `Expression2` or `Essence2Kit`. ## 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](https://docs.bithuman.ai/start/api-secret)). The scheme's environment is for local builds. Every copy of a shipped app carries its secret, so treat it as exposed: fetch it from your backend at startup, keep it in the Keychain, give each app its own secret, and rotate it if usage looks wrong ([What a shipped app holds](https://docs.bithuman.ai/start/api-secret#what-a-shipped-app-holds)). Credits pay for active session time, talking or idle, billed to the second ([pricing](https://docs.bithuman.ai/pricing)). A Mac app built from Xcode's App template turns on App Sandbox. Under *Signing & Capabilities → App Sandbox*, tick **Outgoing Connections (Client)**, or the engines cannot check your secret. ## First frame The [macOS Expression 2 example](https://docs.bithuman.ai/examples/macos-expression-2) is one Swift file. Clone it, fetch the sample avatar, and run it: ```bash git clone https://github.com/bithuman-product/bithuman-examples.git cd bithuman-examples/swift/macos-expression2 ./setup.sh # the wise-pup avatar, the shared engine and a speech clip export BITHUMAN_API_SECRET="" swift run -c release MacOSExpression2 ```
Expected result ```text engine ready: 416x720, isReady=true audio: 325451 samples, 20.34 s generated 407 frames in … s -> out/first-frame.png ``` 407 frames for 20.34 seconds of audio: one frame per 50 ms of speech. The first run prepares the engine for your Mac; keep `Model/staged/` and later runs start faster.
The core of `Sources/main.swift`: ```swift // excerpt: swift/macos-expression2/Sources/main.swift let engine = try Expression2Engine.create( avatarContainer: model.appendingPathComponent("agent.imx"), sharedEngineContainer: model.appendingPathComponent("shared-engine.imx"), stagingDir: model.appendingPathComponent("staged")) // … let started = Date() engine.feed(samples) engine.flushTail() // … var frames = 0, idleTicks = 0 while idleTicks < 100 { var got = false while let (frame, _) = engine.pull() { if frames == 0 { writePNG(frame, width: engine.width, height: engine.height, to: out.appendingPathComponent("first-frame.png")) } frames += 1 got = true } if got { idleTicks = 0 } else { idleTicks += 1; usleep(50_000) } } ``` ## Complete example [macOS Expression 2](https://docs.bithuman.ai/examples/macos-expression-2) walks through the tool above: requirements, your own avatar and audio, and troubleshooting. For a window with a microphone button, the [iOS Expression 2 example](https://docs.bithuman.ai/examples/ios-expression-2) is the same engine in a SwiftUI app. ## Integrate into your app The Swift API is the same on the Mac as on iPhone and iPad: feed 16 kHz mono audio, take frames on your player's clock, end and interrupt replies. The whole table is on [iOS & iPadOS](https://docs.bithuman.ai/platforms/ios#integrate-into-your-app); every entry point is on the [Swift reference](https://docs.bithuman.ai/platforms/swift/reference). On a Mac: - **Files:** add the `.imx` files and engine resources to the app bundle. A sandboxed app reads only its bundle and container. - **Quitting:** call `Essence2Engine.quiesceAll()` from `applicationWillTerminate`. ## Platform notes - **Also on a Mac:** the [CLI](https://docs.bithuman.ai/platforms/cli) renders an avatar or runs a live conversation with no code, and [Python](https://docs.bithuman.ai/platforms/python) renders frames from your own scripts. Both run on Apple silicon. - **Intel Macs** are not supported. ## Performance | Configuration | Hardware | Essence 2 | Expression 2 | Measured | |---|---|---|---|---| | macOS · Swift package | Apple M4 | 4.8× real time | 8.8× real time | Swift package 2.15.0, 2026-09-24 | | macOS · CLI | Apple M4 | 4.2× real time | 8.4× real time | CLI 2.8.1, 2026-09-27 | | macOS · Python | Apple M4 | 6.9× real time | 8.4× real time | bithuman 2.11.12, 2026-09-25 | × real time: seconds of video rendered per second; 1.0× or more holds a live conversation ([method](https://docs.bithuman.ai/performance#desktop)). ## Troubleshooting | Symptom | Cause | Fix | |---|---|---| | `refusing to serve: no API secret was found` | no secret in this shell or scheme | `export BITHUMAN_API_SECRET=…`, or set it in the scheme | | *cannot reach bitHuman to verify your credential* in a Mac app | App Sandbox blocks outgoing connections | tick **Outgoing Connections (Client)** under App Sandbox | | `create` throws before `engine ready` | the model files are missing | run `./setup.sh` from the example folder, so `Model/` holds the three files | | The link step prints about ten `unable to open object file` warnings naming a folder on another machine | debug paths recorded in the published binary | harmless; the tool runs | | The first run is slow | the engine is prepared for this Mac once | keep `Model/staged/` between runs | ## Reference - [Swift reference](https://docs.bithuman.ai/platforms/swift/reference): every Swift and C entry point. - [macOS Expression 2 example](https://docs.bithuman.ai/examples/macos-expression-2) and its [source on GitHub](https://github.com/bithuman-product/bithuman-examples/tree/main/swift/macos-expression2). - [Changelog](https://docs.bithuman.ai/changelog) and [Downloads & versions](https://docs.bithuman.ai/downloads). --- # Android URL: https://docs.bithuman.ai/platforms/android > The Android SDK renders Essence 2 and Expression 2 on Android phones, from one Maven Central dependency. Both models render on the phone: you feed 16 kHz mono speech in and pull picture frames out. After the one-time model download, the only network traffic is usage reporting. Each model is one Maven Central dependency. Why render on the device: - What reaches bitHuman: When the avatar renders in your app on the device and you use your own voice and language services, bitHuman receives usage metering only, never audio, video or conversation text. - What it costs: 2 credits per minute of active session time on the device, against 4 for a bitHuman cloud avatar: about $0.02 and $0.04 a minute at the top-up rate ([pricing](https://docs.bithuman.ai/pricing)). - When the network drops: A session checks your credential when it starts and keeps rendering through a network drop of up to 5 minutes. *Capture: The sofia-ramirez avatar speaking in the essence2-hello app on a Galaxy S25+, at 1080×1920. Captured on Samsung Galaxy S25+ (Android) · essence2-android 0.5.14 · sofia-ramirez (Essence 2) · 2026-09-23.* (https://docs.bithuman.ai/examples/android-essence-2/clip.mp4) | Detail | Expression 2 | Essence 2 | |---|---|---| | **Renders** | [any character from one portrait](https://docs.bithuman.ai/models/expression-2) | [a photoreal person from one portrait](https://docs.bithuman.ai/models/essence-2) | | **Devices** | a physical `arm64-v8a` phone, `minSdk 26` | a physical `arm64-v8a` phone, `minSdk 29` | | **Dependency** | `implementation("ai.bithuman:expression2-android:0.5.2")` | `implementation("ai.bithuman:essence2-android:0.8.1")` | | **Credential** | an [API secret](https://docs.bithuman.ai/start/api-secret), Creator plan or higher | an API secret, Creator plan or higher | | **First-run download** | about 160 MB | 226–281 MB | | **Adds to your APK** | 2.8 MB, plus a 70 MB accelerator runtime you can leave out | 12.1 MB | | **Worked example** | [Android Expression 2](https://docs.bithuman.ai/examples/android-expression-2) | [Android Essence 2](https://docs.bithuman.ai/examples/android-essence-2) | ## Before you start - **JDK 17, Gradle 8.11 or newer and Android Gradle Plugin 8.7 or newer.** - **A physical arm64 phone.** Emulators cannot load the engines. - **Essence 1** is not available on phones: use Essence 2 or Expression 2 on devices ([First generation](https://docs.bithuman.ai/models/first-generation)). ## Install Add Maven Central, restrict the build to `arm64-v8a`, and turn on legacy packaging so the engines' native libraries are extracted to disk. The API secret reaches your code through `BuildConfig`. ```kotlin // settings.gradle.kts dependencyResolutionManagement { repositories { google() mavenCentral() } } // app/build.gradle.kts android { buildFeatures { buildConfig = true } defaultConfig { minSdk = 26 // 29 for Essence 2 ndk { abiFilters += "arm64-v8a" } buildConfigField( "String", "BITHUMAN_API_SECRET", "\"${providers.gradleProperty("bithumanApiSecret").getOrElse("")}\"", ) } packaging { jniLibs { useLegacyPackaging = true } } // required } dependencies { implementation("ai.bithuman:expression2-android:0.5.2") // or: implementation("ai.bithuman:essence2-android:0.8.1") } ``` Put the secret in `~/.gradle/gradle.properties` as `bithumanApiSecret=…`, outside your source tree. `expression2-android` brings the Qualcomm accelerator runtime with it (`com.qualcomm.qti:qnn-litert-delegate:2.49.0` and `com.qualcomm.qti:qnn-runtime:2.49.0`). To keep the APK small and render on the CPU instead, exclude it: ```kotlin implementation("ai.bithuman:expression2-android:0.5.2") { exclude(group = "com.qualcomm.qti") } ``` ## Authenticate Pass your API secret ([create one](https://www.bithuman.ai/developer/api-keys)) in code before you download or create an avatar: `Expression2Credential.set(secret)` for Expression 2, `Essence2Credential.set(secret)` for Essence 2. That one call covers the download and the session. `Expression2Metering.apiSecret` and `Essence2Metering.apiSecret` still work but are deprecated. See [Your API secret](https://docs.bithuman.ai/start/api-secret). Credits pay for active session time, talking or idle, billed to the second ([pricing](https://docs.bithuman.ai/pricing)). > **Warning:** a `buildConfigField` compiles the secret into the APK, where anyone with the file can read it. Use it for local builds only. Every copy of a shipped app carries its secret, so treat it as exposed: fetch it from your backend at startup, give each app its own secret, and rotate it if usage looks wrong ([What a shipped app holds](https://docs.bithuman.ai/start/api-secret#what-a-shipped-app-holds)). ## First frame Expression 2, with the published `wise-pup` avatar (agent code `A23WJF0199`). Call `render` off the main thread. ```kotlin import ai.bithuman.expression2.Expression2Avatar import ai.bithuman.expression2.Expression2Credential import ai.bithuman.expression2.Expression2ModelStore import ai.bithuman.expression2.Expression2Options import android.content.Context import android.graphics.Bitmap /** [pcm16k] is 16 kHz mono float32 in [-1, 1]. */ fun render(context: Context, pcm16k: FloatArray, show: (Bitmap) -> Unit) { Expression2Credential.set(BuildConfig.BITHUMAN_API_SECRET) // before fetch() and create() val model = Expression2ModelStore(context).fetch("A23WJF0199") // ~160 MB, first run only Expression2Avatar.create(context, model, Expression2Options()).use { avatar -> val frame = avatar.newFrameBitmap() // 416 x 720, allocate once avatar.feed(pcm16k) avatar.flushTail() // end of the utterance while (true) { if (avatar.pull(frame) != null) { show(frame); continue } if (!avatar.hasPendingTail && avatar.queuedFrames == 0) break Thread.sleep(10) // null means "not ready yet" } } } ``` Expected: `show` receives 20 frames for each second of audio, and the avatar's lips follow the speech. The first `create()` after install prepares the accelerator once and takes noticeably longer than later launches, which reuse it. Create once, at app start, on a background thread. Essence 2 takes 16-bit little-endian PCM bytes, as a 16 kHz mono WAV stores them, and fills an RGBA `ByteBuffer` sized from the identity: ```kotlin import ai.bithuman.essence2.Essence2Avatar import ai.bithuman.essence2.Essence2Credential import ai.bithuman.essence2.Essence2ModelStore import android.content.Context import java.nio.ByteBuffer fun render(context: Context, pcm16le: ByteArray, show: (ByteBuffer, Int, Int) -> Unit) { Essence2Credential.set(BuildConfig.BITHUMAN_API_SECRET) // before fetch() and create() val identity = Essence2ModelStore(context).fetch("A52DHS2219") // sofia-ramirez; 226–281 MB, first run only Essence2Avatar.create(identity.dir).use { avatar -> val frame = avatar.newFrameBuffer() // width * height * 4, RGBA avatar.feed(pcm16le) avatar.endOfAudio() var idle = 0 while (idle < 100) { // 1 s with no frame = drained if (avatar.pull(frame)) { show(frame, avatar.width, avatar.height); idle = 0; continue } idle++ Thread.sleep(10) } avatar.checkRender() // throws Essence2RenderFailed if the engine stopped // a refused session throws Essence2MeteringRefused from pull() or idle() } } ``` Frame size belongs to the identity (portrait 1080×1920, landscape 1920×1080 or 1280×720). Read `avatar.width` and `avatar.height`; do not hard-code them. ## Complete example Two apps you can clone and run on a phone, each with idle motion between replies: - [Android Expression 2](https://docs.bithuman.ai/examples/android-expression-2): the `wise-pup` sample avatar. - [Android Essence 2](https://docs.bithuman.ai/examples/android-essence-2): the `sofia-ramirez` sample avatar at full resolution. ## Integrate into your app In a live conversation, keep one avatar open and stream into it. | Job | Expression 2 | Essence 2 | |---|---|---| | Audio in | 16 kHz mono `FloatArray`, −1 to 1 | 16 kHz mono 16-bit little-endian PCM `ByteArray` | | Stream audio as it arrives | `feed(chunk)` per chunk | `feed(chunk)` per chunk | | Show frames | `pull(bitmap)`, 20 a second | `pull(buffer)`, 25 a second | | End of a reply | `flushTail()` | `endOfAudio()` | | Idle between replies | `avatar.idleLoop?.next(bitmap)` | `idle(buffer)` | | Interrupt the reply | `resetState(true)` | `resetAudio()` | | Check the session | `Expression2Exception` from `create` or `pull` | `Essence2MeteringRefused` from `pull`/`idle`; `checkRender()` throws `Essence2RenderFailed` if the engine stopped | | Close it | `close()` | `close()` | Resample 24 kHz speech (OpenAI Realtime's) to 16 kHz, and close the avatar when the app leaves the screen: [Companion app](https://docs.bithuman.ai/build/companion-app#resample-speech-to-16-khz). After your API secret is accepted, a network loss does not stop the session for 5 minutes of rendered video. After that, render calls throw a retryable exception until the connection returns. Usage is reported to your account when it does. For a Flutter app, the [Flutter plugin](https://docs.bithuman.ai/platforms/flutter) wraps these engines. ## Platform notes - **Release builds:** `isMinifyEnabled = true` needs nothing extra. Both AARs ship their own keep rules. - **Two models in one app:** `essence2-android` needs `minSdk 29`. Raise the app to 29, or put each model in its own module. - **Threads:** download and `create()` on a background thread. The first download is the size shown above, into app-private storage. - **Check the version you resolved:** Gradle keeps an exact version, so read it back when behaviour differs from this page. ```bash ./gradlew :app:dependencies --configuration releaseRuntimeClasspath | grep ai.bithuman ``` - **Private avatars:** an avatar you created downloads with the secret you set with `Expression2Credential.set` or `Essence2Credential.set`; there is nothing else to pass. ## Performance | Configuration | Hardware | Essence 2 | Expression 2 | Measured | |---|---|---|---|---| | Android | Samsung Galaxy S25+ | 2.0× real time | 2.4× real time | essence2-android 0.7.0 and expression2-android 0.4.10, 2026-09-25, 2026-09-23 | | Android (held 10 min) | Samsung Galaxy S25+ | 1.4× real time | 2.2× real time | essence2-android 0.8.1 and expression2-android 0.5.2, 2026-09-27 | × real time: seconds of video rendered per second; 1.0× or more holds a live conversation ([method](https://docs.bithuman.ai/performance#mobile)). ## Troubleshooting | Symptom | Cause | Fix | |---|---|---| | `Expression2Exception` from `create()` naming the API secret | no secret set | call `Expression2Credential.set(secret)` before `fetch()` and `create()` | | `MeteringRefused` on the first Essence 2 `pull()` | no secret set | call `Essence2Credential.set(secret)` before `fetch()` and `create()` | | `Essence2StoreException` from `fetch()` | no secret was set when the store downloaded | call `Essence2Credential.set(secret)` before `fetch()` | | Expression 2 renders slowly; `acceleratorNote` says no `libQnnTFLiteDelegate.so` | the accelerator runtime was excluded, or legacy packaging is off | keep the dependency whole and set `useLegacyPackaging = true` | | The first Expression 2 `create()` after install is slow | the accelerator prepares the decoder once and keeps it; later launches reuse it | create on a background thread at app start; only the first launch after install pays it (and again after an SDK or OS update) | | Download refused with `401` | the avatar is private | set its owner's API secret with `Expression2Credential.set` or `Essence2Credential.set` | | `409 MODEL_NOT_GENERATED` on download | the agent has no model of that kind yet | [add the model](https://docs.bithuman.ai/api/agents#add-a-model-to-an-existing-agent), then retry | | Manifest merge fails on `minSdk` | `essence2-android` needs `minSdk 29` | raise the module to 29 | | `Unresolved reference: BuildConfig` | the Android Gradle Plugin turns `BuildConfig` off by default | add `buildFeatures { buildConfig = true }` | | `Unresolved reference 'MeteredDoorResolver'` | the resolver's public name is `Essence2MeteredDoorResolver` | you rarely need it: `Essence2Credential.set(secret)` covers downloads. To pass a secret explicitly: `import ai.bithuman.essence2.Essence2MeteredDoorResolver`, then `Essence2ModelStore(context, urlResolver = Essence2MeteredDoorResolver(secret))` | | `UnsatisfiedLinkError` on an emulator | the engines are `arm64-v8a` only | run on a physical arm64 handset | ## Reference - [Android API reference](https://docs.bithuman.ai/platforms/android/reference): every public class in both AARs. - Examples: [Expression 2](https://docs.bithuman.ai/examples/android-expression-2) · [Essence 2](https://docs.bithuman.ai/examples/android-essence-2), complete apps you can clone. - [Flutter](https://docs.bithuman.ai/platforms/flutter): the Flutter plugin, built on these engines. - [Changelog](https://docs.bithuman.ai/changelog) and [Downloads & versions](https://docs.bithuman.ai/downloads). - FFmpeg in `essence2-android` is LGPL: [relink materials](https://docs.bithuman.ai/legal/android-ffmpeg-lgpl). --- # Flutter URL: https://docs.bithuman.ai/platforms/flutter > The Flutter plugin renders Essence 2 and Expression 2 on Android phones, inside a Flutter app. One Flutter dependency gives your app an avatar widget. On Android the plugin runs the same engines as the [Android SDK](https://docs.bithuman.ai/platforms/android), so the avatar renders on the phone: 16 kHz mono speech goes in, and a lip-synced picture comes out as a Flutter `Texture`. Why render on the device: - What reaches bitHuman: When the avatar renders in your app on the device and you use your own voice and language services, bitHuman receives usage metering only, never audio, video or conversation text. - What it costs: 2 credits per minute of active session time on the device, against 4 for a bitHuman cloud avatar: about $0.02 and $0.04 a minute at the top-up rate ([pricing](https://docs.bithuman.ai/pricing)). - When the network drops: A session checks your credential when it starts and keeps rendering through a network drop of up to 5 minutes. | Platform | What the plugin does | |---|---| | **Android** (arm64-v8a, a physical device) | renders Essence 2 and Expression 2 on the phone; the engines resolve from Maven Central, and the example app builds from a clone | | **iOS** (16 or newer) | builds from the published tag after the plugin's bootstrap step. For an iPhone or iPad app that renders on the device, use the [Swift package](https://docs.bithuman.ai/platforms/ios) | | **macOS** (13 or newer, Apple silicon) | builds from the published tag after the bootstrap step and two Homebrew libraries. For a Mac app, the [Swift package](https://docs.bithuman.ai/platforms/macos) is the documented path | ## Before you start | You need | Notes | |---|---| | Flutter 3 with the Android toolchain | JDK 17 and the Android SDK | | A physical Android phone, arm64 | emulators cannot load the engines; Essence 2 needs Android 10 (API 29) or newer | | An [API secret](https://docs.bithuman.ai/start/api-secret) | the Creator plan or higher | ## Install The plugin is published as a tag in the `homebrew-bithuman` repository. Pin it in `pubspec.yaml`: ```yaml dependencies: bithuman: git: url: https://github.com/bithuman-product/homebrew-bithuman.git path: packages/flutter-plugin ref: flutter-plugin-v2.6.21 ``` Then run `flutter pub get`. On Android, Gradle resolves `ai.bithuman:essence2-android` and `ai.bithuman:expression2-android` from Maven Central; nothing else to fetch. For an iOS or macOS build, three more steps: 1. Run the plugin's `scripts/bootstrap.sh` once. It downloads the published engines and checks their sha256. For a git dependency, the plugin's folder is `packages/flutter-plugin` under `~/.pub-cache/git/homebrew-bithuman-…`. 2. Raise the deployment targets: `platform :ios, '16.0'` in `ios/Podfile`, `platform :osx, '13.0'` in `macos/Podfile`, and the Runner targets to match. 3. On macOS only: `brew install llama.cpp onnxruntime`. The plugin links both. ## Authenticate The engines check your API secret when an avatar loads: pass it to `BithumanAvatar.load(…, apiSecret:)`. The example app asks for it on first launch and keeps it in the Android Keystore, or you can pass `--dart-define=BITHUMAN_API_SECRET=…` when you build. A shipped app holds the secret on the device, so give each app its own secret that you can rotate or revoke ([API secrets](https://www.bithuman.ai/developer/api-keys)). Credits pay for active session time, talking or idle, billed to the second ([pricing](https://docs.bithuman.ai/pricing)). ## First frame Build the example app for your phone, with the `wise-pup` sample avatar: ```bash git clone https://github.com/bithuman-product/bithuman-examples.git cd bithuman-examples/app/avatar_chat flutter pub get flutter run --release --dart-define=AGENT_CODE=A23WJF0199 ```
Expected result The app installs on the connected phone, asks for your API secret once, downloads the avatar (about 160 MB, first run only) and shows it idling full screen. Speak, or type a line, and it answers with its lips in sync.
## Complete example The [`avatar_chat` app](https://github.com/bithuman-product/bithuman-examples/tree/main/app/avatar_chat) is a complete voice conversation with idle motion and interruption, in one layout for every platform. From plugin 2.6.20 its voice session connects through bitHuman's [realtime relay](https://docs.bithuman.ai/api/realtime) with your API secret; no token is minted. ## Integrate into your app The avatar is a `Texture` in your widget tree. On Android the first argument to `load` is the agent code: ```dart // excerpt: app/avatar_chat/lib/main.dart (bithuman-examples) import 'package:bithuman/bithuman.dart'; await BithumanAvatar.setExpression2AgentDir('A23WJF0199'); final avatar = await BithumanAvatar.load('A23WJF0199', engine: 'expression2', apiSecret: secret); Texture(textureId: avatar.textureId); // the avatar in your layout avatar.pushAudio(pcm); // Int16List, 16 kHz mono speech avatar.interrupt(); // cut the current reply await avatar.dispose(); // release the engine ``` | Job | Call | |---|---| | Show the avatar | `Texture(textureId: avatar.textureId)`; `frameWidth` and `frameHeight` give its size | | Stream speech | `pushAudio(Int16List)`, 16 kHz mono | | Know it is ready | `isReady`; audio pushed before it is dropped | | Interrupt the reply | `interrupt()` | | Stop | `dispose()` | | Pick the model | `engine: 'expression2'` or `engine: 'essence2'` | ## Platform notes - **Two models in one app:** `essence2-android` needs `minSdk 29`; raise the app to 29. - **Your own voice pipeline:** any speech your stack produces works, as 16 kHz mono PCM through `pushAudio`. - **Versions:** each plugin tag fixes the Android SDK versions it uses. The current tag and its line are on [Downloads & versions](https://docs.bithuman.ai/downloads). ## Performance The plugin runs the Android SDK's own engines (the same native libraries), so the Android rows apply: | Configuration | Hardware | Essence 2 | Expression 2 | Measured | |---|---|---|---|---| | Android | Samsung Galaxy S25+ | 2.0× real time | 2.4× real time | essence2-android 0.7.0 and expression2-android 0.4.10, 2026-09-25, 2026-09-23 | | Android (held 10 min) | Samsung Galaxy S25+ | 1.4× real time | 2.2× real time | essence2-android 0.8.1 and expression2-android 0.5.2, 2026-09-27 | × real time: seconds of video rendered per second; 1.0× or more holds a live conversation ([method](https://docs.bithuman.ai/performance#mobile)). ## Troubleshooting | Symptom | Cause | Fix | |---|---|---| | `UnsatisfiedLinkError` on an emulator | the engines are `arm64-v8a` only | run on a physical arm64 phone | | The app shows a refusal and asks for a secret | no API secret, or a rejected one | enter a valid secret, or build with `--dart-define=BITHUMAN_API_SECRET=…` | | Manifest merge fails on `minSdk` | Essence 2 needs `minSdk 29` | raise the app to 29 | | `pod install` fails on iOS or macOS | the deployment target is below iOS 16 or macOS 13 | raise the Podfile platform and the Runner targets | | The iOS or macOS build cannot find the engines | the bootstrap step was skipped | run `scripts/bootstrap.sh` in the plugin's folder, then build again | | A macOS link error names `llama` or `onnxruntime` | the Homebrew libraries are missing | `brew install llama.cpp onnxruntime` | ## Reference - [Android](https://docs.bithuman.ai/platforms/android): the engines under the plugin, their API and their settings. - [`avatar_chat` example](https://github.com/bithuman-product/bithuman-examples/tree/main/app/avatar_chat) and the [plugin source](https://github.com/bithuman-product/homebrew-bithuman/tree/main/packages/flutter-plugin). - [Changelog](https://docs.bithuman.ai/changelog) and [Downloads & versions](https://docs.bithuman.ai/downloads). --- # Web: embed and WebGPU URL: https://docs.bithuman.ai/platforms/web > Put a live, talking avatar on any web page with one iframe. It renders in the bitHuman cloud, or in the visitor's tab with WebGPU. The web surface is one URL: `https://www.bithuman.ai/embed/`. Put it in an ` ``` Expected: the avatar appears, asks for the microphone, and answers when you speak. Keep the `*` in `allow`, or the microphone is blocked. To try it without a page, open [https://www.bithuman.ai/embed/A23WJF0199](https://www.bithuman.ai/embed/A23WJF0199). ## Complete example A whole page with a live avatar: one HTML file and a local web server. ### Requirements | You need | Notes | |---|---| | A current browser | Chrome, Edge, Safari or Firefox | | A local web server | the page must be served over `http://localhost` or HTTPS for the microphone to work | | Nothing else | no account for the sample avatar; your own agent works the same way while its Anonymous Share setting is on, and its sessions bill you | ### Get the code Save this as `index.html`: ```html bitHuman web embed ``` ### Run it ```bash python3 -m http.server 8765 --bind 127.0.0.1 ``` Open `http://127.0.0.1:8765/` and allow the microphone. ### Expected output The avatar greets you within a few seconds. Speak, or type into the **Type or speak…** box, and it answers out loud with its lips in sync. The red button ends the session. ### How it works The iframe loads the hosted viewer for agent `A23WJF0199`. The viewer opens a real-time session: your microphone audio goes to the agent, and the agent's voice and video come back. `allow="microphone *"` lets the iframe ask for the microphone; without the `*` the browser blocks it. URL parameters are on [Web](https://docs.bithuman.ai/platforms/web); session events on [Embedding](https://docs.bithuman.ai/api/embedding). ### Make it your own - **Your own avatar:** replace `A23WJF0199` with your agent code. Anyone with the code can open it and sessions bill your account; turn off Anonymous Share in the agent's sharing settings to stop that. - **Push what it says:** from your backend, `POST /v1/agent/{code}/speak` makes a live avatar say a line ([Agents](https://docs.bithuman.ai/api/agents)). - **Size and layout:** any width and height work; keep roughly a 7:12 portrait shape for Expression 2 avatars. ## Integrate into your app Add parameters to the URL: | Parameter | Values | Effect | |---|---|---| | `render` | `cloud` (default), `local` | Where the avatar renders: our servers, or the visitor's tab | | `rendering_mode` | `browser`, `avatar` | Long form of `render=local`; `avatar` renders in the tab and lip-syncs the visitor's own microphone, with no conversation | | `greetingLang` | a language code, for example `es` | Language of the first greeting | | `greetingMsg` | text | The first thing the avatar says | A private agent also takes `token`, and a session can pin its model with `model`; both are on [Embedding](https://docs.bithuman.ai/api/embedding). Other parameters are ignored. ### Render in the visitor's tab (WebGPU) `render=local` renders the avatar in the visitor's browser tab with WebGPU. It works for any avatar you can embed and is off by default: without it, every session renders in the bitHuman cloud and streams to the page. - **One download:** the avatar's web bundle (50–200 MB) downloads to the browser once, then comes from the cache. Tell visitors before it starts on a metered connection. - **Fallback:** a browser without a usable GPU is switched to cloud rendering, so every visitor gets lip-sync. - **Where the conversation runs:** with the web embed, the conversation runs on bitHuman's servers, even when the avatar renders in the tab (`render=local`). - **Private agents:** the embed token the iframe already uses covers it ([Embedding](https://docs.bithuman.ai/api/embedding)). *Diagram: The web embed with render=local.* With render=local the avatar renders in the visitor's browser tab with WebGPU. The conversation runs on bitHuman's servers, even when the avatar renders in the tab: the microphone audio goes to bitHuman and the voice reply comes back. Check for a usable GPU before you choose `render=local`: ```js async function hasRealGPU() { if (!navigator.gpu) return false; const once = async () => { try { return (await navigator.gpu.requestAdapter()) ?? null; } catch { return null; } }; const adapter = (await once()) ?? (await once()); // the first request can return null while the GPU starts return !!adapter && adapter.isFallbackAdapter !== true && adapter.info?.isFallbackAdapter !== true; } const mode = (await hasRealGPU()) ? "local" : "cloud"; iframe.src = `https://www.bithuman.ai/embed/A23WJF0199?render=${mode}`; ``` Do not send `Cross-Origin-Embedder-Policy` from the page that holds the iframe: the embed does not send one itself, so the browser refuses to load it. ### React and other frameworks There is no npm package: the embed is an iframe in any framework. In React: ```jsx export function Avatar({ code }) { return ``` Open the page and talk to it. Keep the `*` in `allow`, or the microphone is blocked. For your own site in production, mint an [embed token](https://docs.bithuman.ai/api/embedding). ### Render a talking video ```bash curl -s -X POST https://api.bithuman.ai/v1/video/generate \ -H "api-secret: $BITHUMAN_API_SECRET" -H "Content-Type: application/json" \ -d '{"agent_code": "A80HVD8577", "model": "expression-2", "input": {"type": "text", "text": "Welcome to our store."}}' ``` Poll `GET /v1/video/{job_id}` until `status` is `completed`, then download `video_url` ([Talking video](https://docs.bithuman.ai/api/video)). ## Troubleshooting | Symptom | Cause | Fix | |---|---|---| | `402 INSUFFICIENT_BALANCE` | the balance is below the creation cost | [top up](https://docs.bithuman.ai/pricing#top-up-credits) | | `validate.sh` prints `"valid": false` | the secret is wrong or revoked | create a new one | | The status stays at `lip_sync` for a long time | that is the training step (about 2 hours) | keep polling | | `404` from `speak.sh` or `/v1/agent/{code}/speak` | the agent is not yours, or it has no live session | the message says which; open its embed page first | All error codes: [Errors](https://docs.bithuman.ai/api/errors). ## Reference - [API reference](https://docs.bithuman.ai/api/reference): every endpoint, generated from the OpenAPI spec. - [Agents](https://docs.bithuman.ai/api/agents) · [Talking video API](https://docs.bithuman.ai/api/video) · [Text to speech](https://docs.bithuman.ai/api/text-to-speech) · [Embedding](https://docs.bithuman.ai/api/embedding) · [Errors](https://docs.bithuman.ai/api/errors)