TheStage Android SDK¶
Attention
Access to the Android SDK requires an API token from the TheStage AI Platform. Pricing is by arrangement — open a Service Request at app.thestage.ai/contact; there is no public rate card. See TheStage Android SDK Product Terms and Licensing & Device Identity.
LICENSE in the repository; commercial summary: TheStage Android SDK Product TermsOverview¶
On-device speech, language and audio inference for Android on Qualcomm
Snapdragon. The SDK runs compiled engines on the Hexagon NPU (HTP, via the QNN
runtime) with automatic GPU / CPU fallback, pulls engines from HuggingFace
on first use, and exposes a unified infer / infer_stream API for every
pipeline. No server in the hot path.
There are two consumption surfaces: the native Kotlin singleton
(TheStageAI) and the Flutter plugin (thestage_android_sdk →
TheStageFlutterSDK). They mirror each other one-to-one; Dart consumers
always go through the JSON path.
Version |
1.0.0 — pin this tag ( |
Platform |
Android, |
Backend |
Hexagon NPU (QNN / HTP) with automatic GPU / CPU fallback |
Token |
one online check per process (cached in-memory); a fresh launch re-validates online |
Engines ship per model through HuggingFace (or Google Play AI packs — see
AI Packs (Google Play delivery)); each pipeline page lists its models in a
Supported models table, and model cards (contracts + acknowledgments) live
at huggingface.co/TheStageAI. An
on-device chat-LLM pipeline (TheStageAI/Qwen3-0.6B, LFM2.5-230M) is
coming soon.
Prerequisites¶
Requirement |
Minimum |
Notes |
|---|---|---|
Android device |
Android 9 (API 28) |
physical Qualcomm Snapdragon, arm64-v8a |
Android SDK (compile) |
API 35 |
|
Flutter (only for the Flutter examples) |
3.24 |
with a matching Dart 3.5+ |
JDK |
17 |
The NPU backend requires a Snapdragon SoC with a Hexagon DSP; on other hardware the SDK falls back to GPU/CPU.
Obtaining an API Token¶
Every SDK call begins with initialize(api_token = ...). You need a valid
API token from the TheStage AI Platform before any pipeline will load.
Sign in at app.thestage.ai.
Go to Profile → API tokens tab.
Click Generate API token, add a description, and press Generate token.
Copy the token immediately — you will not be able to view it again after leaving the page.
For the full walkthrough with screenshots, see SSH Keys and API Tokens.
Once you have a token, pass it to the SDK at startup:
Kotlin:
import ai.thestage.qlip.TheStageAI
TheStageAI.registerContext(context)
TheStageAI.initialize(api_token = "your-api-token")
Flutter:
import 'package:thestage_android_sdk/thestage_android_sdk.dart';
await TheStageFlutterSDK.initialize(api_token: 'your-api-token');
registerContext (Kotlin only) takes any Android Context — your
Application, an Activity, or a Service. The SDK keeps only the
applicationContext (no Activity leak) and uses it to reach app-private
storage, preferences, and other platform services. Call it once per process,
before initialize. Flutter apps never call it — the plugin registers the
context automatically when the engine attaches.
The token is validated online once per process (cached in-memory); a fresh app
launch re-validates online — there is no offline grace window, so the device
must be reachable on first use. If initialize fails with
Token validation failed, check the token and the network connection. A
billable seat is the pair (apiToken, deviceId) — see
Licensing & Device Identity for how devices are identified and counted (do not
depend on how the device identity is derived).
Attention
Keep your API token secret. Never commit it to source control. For Flutter
apps, use --dart-define-from-file=secrets.json and add secrets.json
to .gitignore — see Secrets below.
Example Apps¶
The repository ships ready-to-run Flutter examples under examples/ — each
has its own README.md with app-specific notes.
Example |
Description |
|---|---|
Streaming neural TTS demo. Start here. |
|
Full voice-assistant loop, mic → VAD → STT → LLM → streaming TTS. |
|
The voice agent plus custom graph nodes + ephemeral VLM captions + screen recording. |
|
On-device TTS / ASR / VLM benchmark with JSON export. |
Alongside the examples, the repo contains TheStageCore.aar (the pre-built
SDK binary: inference engine, license gate, ONNX Runtime / QNN / Genie
runtimes — opaque; you link it, you don’t build it), onnxruntime-android.aar
(the QNN-enabled ONNX Runtime AAR the core links against, supplied by the app
at runtime), plugin/thestage_android_sdk/ (the Flutter plugin over platform
channels; bundles the native SDK, nothing to build — Dart entrypoint
plugin/thestage_android_sdk/lib/thestage_android_sdk.dart), docs/ (per-pipeline
reference guides), scripts/setup.sh (one-time host setup: AAR symlinks +
secrets bootstrap for the Flutter examples) and VERSION (the SDK line this
checkout ships, 1.0.0).
To run an example:
# 1. One-time host setup: symlink the AARs into the plugin and
# bootstrap each example's secrets.json. Idempotent.
./scripts/setup.sh
# 2. Drop your API keys into the example you want to run.
cp examples/tts_front_stream/secrets.example.json \
examples/tts_front_stream/secrets.json
$EDITOR examples/tts_front_stream/secrets.json
# 3. Build & run on a connected Snapdragon device.
cd examples/tts_front_stream
flutter pub get
flutter run --release \
--dart-define-from-file=secrets.json \
-d <YOUR_DEVICE_ID>
flutter devices lists attached devices. examples/voice_agent follows
the same recipe (it additionally needs OPENAI_API_KEY in its
secrets.json).
Integration: Native Kotlin (Gradle)¶
You can consume the AARs directly from a Kotlin/Android app, no Flutter
involved. Copy the two prebuilt AARs into your app module’s libs/.
The Qualcomm QNN runtime (the signed per-SoC HTP skel libraries the NPU backend
needs) is published on Qualcomm’s public Maven repository — no login required.
Declare that repository in your settings.gradle.kts alongside the usual
ones:
// settings.gradle.kts
dependencyResolutionManagement {
repositories {
google()
mavenCentral()
// Qualcomm QNN / QAIRT runtime artifacts (public, no auth).
maven { url = uri("https://qpm-download.qualcomm.com/maven/release") }
}
}
Then in your app module’s build.gradle.kts:
dependencies {
// The precompiled TheStage core (opaque engine/license/runtime)
// and the QNN-enabled ONNX Runtime AAR.
implementation(files("libs/TheStageCore.aar"))
implementation(files("libs/onnxruntime-android.aar"))
// Qualcomm QNN runtime — 19 libs: libQnnHtp / HtpPrepare / System /
// Gpu / Dsp plus the per-SoC HTP skel + stub libraries
// (V68, V69, V73, V75, V79, V81, and Dsp V66). Resolved from the
// Qualcomm Maven repo declared above.
implementation("com.qualcomm.qti:qnn-runtime:2.42.0")
// Transitive deps of the core (not pulled via files(...)).
implementation("com.google.code.gson:gson:2.11.0")
implementation("com.squareup.okhttp3:okhttp:4.12.0")
implementation(
"org.jetbrains.kotlinx:kotlinx-coroutines-android:1.9.0"
)
}
Warning
The qnn-runtime Maven artifact does not contain the Genie backend
libraries. If you use any Genie/Stagenie-backed pipeline (the on-device LLM
and NeuTTS Genie paths), you must additionally copy four QAIRT runtime libs
from a Qualcomm QAIRT SDK install — see
Qualcomm QAIRT Runtime Libs (Genie Backend) below.
Because both TheStageCore.aar and onnxruntime-android.aar bundle native
.so libraries, add a packaging block to your app module so the merge
picks one copy of each duplicate, and restrict the ABI to arm64-v8a:
android {
defaultConfig {
minSdk = 28
ndk { abiFilters += "arm64-v8a" }
}
packaging {
jniLibs.pickFirsts.add("lib/arm64-v8a/libonnxruntime.so")
jniLibs.pickFirsts.add("lib/arm64-v8a/libonnxruntime4j_jni.so")
jniLibs.pickFirsts.add("lib/arm64-v8a/libc++_shared.so")
}
}
QNN’s FastRPC skel libraries load from an APK only when they are extracted at
install time, so set android:extractNativeLibs="true" on your
<application> (or useLegacyPackaging = true in the jniLibs
packaging block). Then drive the singleton from Kotlin — register the app
Context once, then initialize with your token:
// Once, at app start (e.g. Application.onCreate).
TheStageAI.registerContext(context)
// Suspend — validates the token online (once per process).
TheStageAI.initialize(api_token = "th_…")
TheStageAI.start_model(
model_name = "stt",
engines_path = "TheStageAI/thewhisper-large-v3-turbo",
)
val result = TheStageAI.infer(
model_name = "stt",
input_json = mapOf(
"audio" to pcm_16k_mono, // FloatArray, mono, [-1, 1]
"language" to "en",
),
)
registerContext is native-only (the Flutter plugin registers the
Context for you). initialize, start_model, infer and
infer_stream are suspend functions — call them from a coroutine.
Qualcomm QAIRT Runtime Libs (Genie Backend)¶
The Genie/Stagenie backend that drives the on-device LLM and the NeuTTS Genie
path depends on four native libraries that are not in the
com.qualcomm.qti:qnn-runtime Maven artifact and that this SDK does not
redistribute:
Library |
Role |
|---|---|
|
GenAI transformer backend |
|
GenAI transformer CPU op package |
|
GenAI transformer model backend |
|
QNN CPU fallback backend |
The Genie generation runtime itself is not in this list — it ships as
libStagenie.so inside TheStageCore.aar (a patched Genie build). You do
not need stock libGenie.so from QAIRT.
You must obtain these from a Qualcomm QAIRT SDK 2.42 install
(e.g. ~/Qualcomm/AIStack/QAIRT/2.42.0.251225, under
lib/aarch64-android/) and copy them into your app’s — or the plugin’s —
jniLibs/arm64-v8a/. The bundled scripts/setup.sh does this for you when
the QAIRT env var points at your install:
QAIRT=~/Qualcomm/AIStack/QAIRT/2.42.0.251225 ./scripts/setup.sh
Getting the QAIRT SDK: download the Qualcomm AI Runtime SDK (QAIRT —
the Qualcomm AI Engine Direct / QNN runtime), version 2.42.0, from
Qualcomm’s developer site via Qualcomm Package Manager
(qpm.qualcomm.com — a free Qualcomm account is
required; search for “Qualcomm AI Runtime SDK”). Install it, then set QAIRT
to the versioned install directory
(e.g. ~/Qualcomm/AIStack/QAIRT/2.42.0.251225) and re-run setup.sh; the
libs it copies live under $QAIRT/lib/aarch64-android/.
Version 2.42.0 is required — the shipped libStagenie.so inside
TheStageCore.aar is built against QAIRT 2.42.0 and is coupled to that
runtime. Other QAIRT versions produce context binaries / ABI the shipped
runtime rejects. Without these libs, any Genie pipeline crashes at first use
with dlopen failed: library "libQnnGenAiTransformer.so" not found.
The Snapdragon-only QNN NPU backend (the qnn-runtime Maven artifact above)
is what everything else uses; only the Genie pipelines need this extra QAIRT
copy step.
Integration: Flutter Plugin¶
Add the plugin as a git: dependency in your app’s pubspec.yaml, pinned
to the tag:
dependencies:
thestage_android_sdk:
git:
url: https://github.com/TheStageAI/AndroidSDK.git
path: plugin/thestage_android_sdk
ref: v1.0.0
The plugin declares the prebuilt AARs compileOnly, so your app module
must supply them at runtime. Copy TheStageCore.aar and the ONNX Runtime AAR
into your app’s libs/ and wire them up as in the
Integration: Native Kotlin (Gradle) section above, then:
import 'package:thestage_android_sdk/thestage_android_sdk.dart';
await TheStageFlutterSDK.initialize(api_token: 'th_…');
await TheStageFlutterSDK.start_model(
model_name: 'stt',
engines_path: 'TheStageAI/thewhisper-large-v3-turbo',
);
final result = await TheStageFlutterSDK.infer(
model_name: 'stt',
input_json: {
'audio': pcm_16k_mono, // Float32List, mono, [-1, 1]
'language': 'en',
},
);
print(result[0]['transcription']);
The fastest way to see a real app is to copy one of the examples/ apps.
Kotlin / Flutter Parity¶
The Kotlin singleton (TheStageAI) and the Flutter TheStageFlutterSDK
mirror each other one-to-one. Dart consumers always go through the JSON path.
Operation |
Kotlin |
Flutter (Dart) |
|---|---|---|
Initialize |
|
|
Start a model |
|
|
Stop a model |
|
|
Single-shot inference |
|
|
Streaming inference |
|
|
Push text into a TTS stream |
|
|
Cancel a running stream |
|
|
Load progress |
|
Single global stream |
Audio buffer type |
|
|
Both surfaces use the same model_name strings and the same input_json
shape; the Flutter path is JSON-only.
Load Progress¶
All loaders emit progress through four phases with a monotonic fraction in
0…1:
Phase |
Fraction band |
Notes |
|---|---|---|
|
0.00 – 0.70 |
HuggingFace repo download (skipped on cache hit) |
|
0.70 – 0.85 |
Bundle unpack to local cache (skipped on cache hit) |
|
0.85 – 0.99 |
Pipeline construction |
|
1.00 (terminal) |
Emitted on success only |
Flutter — events from every active start_model are multiplexed through
one global Stream. Filter by model_name to disambiguate concurrent
loads:
TheStageFlutterSDK.on_progress.listen((event) {
// event['model_name'] : String — model handle passed to start_model
// event['phase'] : String — 'downloading' | 'extracting' | 'loading' | 'ready'
// event['progress'] : double — 0.0 … 1.0, monotonic
});
A complete first-run example — subscribe before start_model so the
download / extract / load phases are reported as the engines arrive:
import 'package:thestage_android_sdk/thestage_android_sdk.dart';
await TheStageFlutterSDK.initialize(api_token: 'your-api-token');
// Subscribe to load progress for any model_name.
TheStageFlutterSDK.on_progress.listen((event) {
if (event['model_name'] != 'stt') return;
print('[stt] ${event['phase']} ${(event['progress'] as double) * 100}%');
});
await TheStageFlutterSDK.start_model(
model_name: 'stt',
engines_path: 'TheStageAI/thewhisper-large-v3-turbo',
);
final result = await TheStageFlutterSDK.infer(
model_name: 'stt',
input_json: {
'audio': pcm_16k_mono, // Float32List, mono, [-1, 1]
'language': 'en',
},
);
print(result[0]['transcription']);
The phase strings, fraction bands and terminal contract are identical on both surfaces. See the per-model guides (e.g. TTS (NeuTTS)) for the full event contract.
Audio I/O Contract¶
All audio crossing the public SDK surface — VAD input, Whisper input, NeuTTS
output — uses PCM float, mono, samples normalized to [-1.0, 1.0]. On
Kotlin this is FloatArray; the Flutter plugin marshals the same data as
Float32List (never Float64List). Sample rate depends on the pipeline:
Pipeline |
Direction |
Sample rate |
Frame / chunking |
Notes |
|---|---|---|---|---|
Silero VAD |
input |
16 000 Hz |
exactly 512 samples per call (32 ms) |
Stateful LSTM. Reset state between independent utterances; the model keeps a 64-sample internal carry-over, so you don’t overlap chunks yourself. |
Whisper |
input |
16 000 Hz |
any length |
Long audio is auto-split into bundle-defined windows (the shipping
|
NeuTTS |
output |
24 000 Hz |
streamer emits per-sentence chunks (variable length); batch mode emits one buffer of full duration |
NeuCodec drives the rate ( |
Speaker ID |
input |
16 000 Hz |
2.0 s window (pad / trim) |
Practical consequences:
The mic stack must run at 16 kHz mono Float for VAD and ASR.
TTS output is always 24 kHz, even though VAD/ASR are 16 kHz. Resample TTS to 16 kHz or drive the player at 24 kHz if you route everything through one playback path.
Secrets¶
The Flutter example apps read tokens at build time via
String.fromEnvironment(...) and --dart-define-from-file=secrets.json.
Each ships a secrets.example.json template — copy it to secrets.json
and fill in your keys. secrets.json is covered by .gitignore; real keys
never belong in source.
Also see¶
ASR (Whisper) — Whisper speech-to-text with automatic VAD chunking and long-audio stitching
VLM (Vision-Language) — on-device image + prompt → text (LFM2.5-VL), batch and streaming
TTS (NeuTTS) — multilingual (9 languages) and nano (English) neural TTS with batch + push-based streaming
VAD (Voice Activity Detection) — Silero VAD: stateful per-chunk speech detection
Streaming — TTS / LLM streaming patterns, back-pressure, sentence segmentation, Flutter consumers
Voice Agent — end-to-end voice assistant (VAD → STT → LLM → TTS) with neural end-of-turn detection and barge-in
Model Management — lifecycle, availability probing, prefetch, revisions, the on-device model cache, per-component load/unload, bundled-engine paths, process-memory reporting
AI Packs (Google Play delivery) — ship models via Google Play AI packs (
aipack://engines source): pack modules, device targeting, delivery modes, availabilitySpeaker Embedding — ReDimNet2 speaker-id: enroll + cosine verification, and voice-agent speaker gating
Licensing & Device Identity — how
(apiToken, deviceId)is derived on Android, what counts as a device, reinstall behavior, device-integrity enforcement, online-validation behaviorTheStage Android SDK Product Terms — commercial / licensing summary: seat model and pricing via Service Request (no public rate card)
Logging & Diagnostics — the diagnostics ring, session log file, and the Flutter
logsstream (no user content, sanitized paths)