@dvai-bridge/android 4.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (30) hide show
  1. package/LICENSE +51 -0
  2. package/README.md +199 -0
  3. package/android/build.gradle +165 -0
  4. package/android/gradle.properties +5 -0
  5. package/android/settings.gradle +1 -0
  6. package/android/src/androidTest/java/co/deepvoiceai/bridge/RealModelIntegrationTest.kt +162 -0
  7. package/android/src/main/AndroidManifest.xml +3 -0
  8. package/android/src/main/java/co/deepvoiceai/bridge/BackendKind.kt +21 -0
  9. package/android/src/main/java/co/deepvoiceai/bridge/BackendSelector.kt +28 -0
  10. package/android/src/main/java/co/deepvoiceai/bridge/BoundServer.kt +39 -0
  11. package/android/src/main/java/co/deepvoiceai/bridge/DVAIBridge.kt +642 -0
  12. package/android/src/main/java/co/deepvoiceai/bridge/DVAIBridgeConfig.kt +119 -0
  13. package/android/src/main/java/co/deepvoiceai/bridge/DVAIBridgeError.kt +51 -0
  14. package/android/src/main/java/co/deepvoiceai/bridge/OffloadProxy.kt +574 -0
  15. package/android/src/main/java/co/deepvoiceai/bridge/ProgressBroadcaster.kt +55 -0
  16. package/android/src/main/java/co/deepvoiceai/bridge/ProgressEvent.kt +25 -0
  17. package/android/src/main/java/co/deepvoiceai/bridge/ReactiveState.kt +60 -0
  18. package/android/src/main/java/co/deepvoiceai/bridge/license/Audience.kt +134 -0
  19. package/android/src/main/java/co/deepvoiceai/bridge/license/Discovery.kt +146 -0
  20. package/android/src/main/java/co/deepvoiceai/bridge/license/LicenseTypes.kt +158 -0
  21. package/android/src/main/java/co/deepvoiceai/bridge/license/LicenseValidator.kt +400 -0
  22. package/android/src/main/java/co/deepvoiceai/bridge/license/PublicKeys.kt +91 -0
  23. package/android/src/test/java/co/deepvoiceai/bridge/BackendSelectorTest.kt +76 -0
  24. package/android/src/test/java/co/deepvoiceai/bridge/CapabilityPrecheckTest.kt +117 -0
  25. package/android/src/test/java/co/deepvoiceai/bridge/DVAIBridgeAPIShapeTest.kt +86 -0
  26. package/android/src/test/java/co/deepvoiceai/bridge/OffloadProxyDecisionTest.kt +144 -0
  27. package/android/src/test/java/co/deepvoiceai/bridge/OffloadProxyForwardingTest.kt +327 -0
  28. package/android/src/test/java/co/deepvoiceai/bridge/ProgressBroadcasterTest.kt +56 -0
  29. package/android/src/test/java/co/deepvoiceai/bridge/license/LicenseValidatorTest.kt +539 -0
  30. package/package.json +19 -0
package/LICENSE ADDED
@@ -0,0 +1,51 @@
1
+ # Deep Voice Ai Limited - Software License Agreement
2
+
3
+ **Version 1.0.0**
4
+
5
+ This License Agreement governs the use of the DVAI-Bridge software (the "Software"). By downloading, installing, or using the Software, you agree to be bound by the terms of this License.
6
+
7
+ ---
8
+
9
+ ## 1. LICENSE GRANTS
10
+
11
+ ### 1.1 Development and Personal Use (Free Tier)
12
+ Deep Voice Ai Limited ("Licensor") grants you a non-exclusive, non-transferable, royalty-free license to use the Software solely for:
13
+ - Internal development and testing purposes.
14
+ - Non-commercial personal projects.
15
+ - Academic and non-profit research.
16
+
17
+ ### 1.2 Commercial Use (Paid Tier)
18
+ Any use of the Software for **Commercial Purposes** requires a separate, paid Commercial License from Licensor. "Commercial Purposes" include:
19
+ - Use in production environments.
20
+ - Integration into revenue-generating products or services.
21
+ - Distribution to third-party customers for a fee.
22
+ - Use by an entity with more than $100,000 USD in annual revenue.
23
+
24
+ To obtain a Commercial License, contact `info@deepvoiceai.co` or visit `https://deepvoiceai.co/licensing`.
25
+
26
+ ---
27
+
28
+ ## 2. RESTRICTIONS
29
+ Except as expressly permitted, you may not:
30
+ - Sublicense, rent, lease, or resell the Software without express permission.
31
+ - Remove any proprietary notices or branding from the Software.
32
+ - Use the Software for any illegal or malicious purposes.
33
+
34
+ ---
35
+
36
+ ## 3. INTELLECTUAL PROPERTY
37
+ The Software is owned by **Deep Voice Ai Limited** and is protected by copyright and intellectual property laws. This agreement does not transfer ownership of the Software.
38
+
39
+ ---
40
+
41
+ ## 4. NO WARRANTY
42
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED. IN NO EVENT SHALL THE LICENSOR BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE.
43
+
44
+ ---
45
+
46
+ ## 5. GOVERNING LAW
47
+ This License shall be governed by and construed in accordance with the laws of the jurisdiction where Deep Voice Ai Limited is registered.
48
+
49
+ ---
50
+
51
+ © 2026 Deep Voice Ai Limited. All rights reserved.
package/README.md ADDED
@@ -0,0 +1,199 @@
1
+ ![DVAI-Bridge](/assets/banner.png)
2
+
3
+ # DVAI-Bridge
4
+
5
+ <!-- [![Smoke — real models](https://github.com/Westenets/dvai-bridge/actions/workflows/smoke-real-models.yml/badge.svg?branch=main)](https://github.com/Westenets/dvai-bridge/actions/workflows/smoke-real-models.yml) -->
6
+
7
+ [![License](https://img.shields.io/badge/License-Commercial-blue.svg)](LICENSE) ![Node.js](https://img.shields.io/badge/Node.js-22+-green?logo=node.js) ![TypeScript](https://img.shields.io/badge/TypeScript-5.6+-blue?logo=typescript) ![Swift](https://img.shields.io/badge/Swift-5.9+-F05138?logo=swift) ![Kotlin](https://img.shields.io/badge/Kotlin-2.0+-7F52FF?logo=kotlin) ![Flutter](https://img.shields.io/badge/Flutter-3.39+-02569B?logo=flutter) ![.NET](https://img.shields.io/badge/.NET-10.0_LTS-512BD4?logo=dotnet)
8
+
9
+ > **The local OpenAI server you embed inside your app.**
10
+ > One library. One HTTP wire. Every platform. Zero install for your users.
11
+
12
+ **Docs:** [dvai-bridge.deepvoiceai.co](https://dvai-bridge.deepvoiceai.co)
13
+
14
+ ```ts
15
+ import { DVAI } from "@dvai-bridge/core";
16
+ import OpenAI from "openai";
17
+
18
+ const dvai = new DVAI({ backend: "transformers" });
19
+ await dvai.initialize();
20
+
21
+ const openai = new OpenAI({ baseURL: dvai.baseUrl, apiKey: "ignored" });
22
+ await openai.chat.completions.create({
23
+ model: dvai.transformersModelId,
24
+ messages: [{ role: "user", content: "Hello!" }],
25
+ });
26
+ ```
27
+
28
+ That's it. A real OpenAI-compatible server is now running inside your app's
29
+ own process. Point any OpenAI client — LangChain, the OpenAI SDK, the Vercel
30
+ AI SDK, anything — at `dvai.baseUrl` and your agent code keeps working.
31
+
32
+ Built by **[Deep Voice AI](https://deepvoiceai.co)**.
33
+
34
+ ---
35
+
36
+ ## Why it exists
37
+
38
+ Local AI works beautifully on a laptop with **Ollama + LangChain**. Then you
39
+ try to ship the app and your users don't have Ollama. Mobile can't run it.
40
+ Corporate IT won't add another daemon. So you reinvent the same plumbing —
41
+ spawn an inference engine, bind a port, translate to OpenAI HTTP, handle
42
+ CORS, manage lifecycle, wrap the accelerator of the day per platform — and
43
+ do it all over again for every target OS.
44
+
45
+ DVAI-Bridge is that plumbing, packaged as a library, for every client
46
+ platform.
47
+
48
+ ---
49
+
50
+ ## What you get
51
+
52
+ - **One OpenAI HTTP surface.** Bound on `127.0.0.1` (or `0.0.0.0` for
53
+ device-to-device). Streaming, embeddings, models, recovery — all built in.
54
+ - **Six SDKs.** `@dvai-bridge/core` + `react` + `vanilla` + `capacitor`,
55
+ `DVAIBridge` (Swift / iOS), `co.deepvoiceai:dvai-bridge` (Kotlin / Android),
56
+ `@dvai-bridge/react-native`, `dvai_bridge` (Flutter), `co.deepvoiceai.dvai-bridge` (.NET).
57
+ - **Nine backends.** WebLLM, Transformers.js, llama.cpp, Apple Foundation
58
+ Models, MLX, CoreML / ANE, MediaPipe LLM, LiteRT, ONNX Runtime GenAI —
59
+ selected per-platform, invisible to your agent code.
60
+ - **Native acceleration** wherever it runs: WebGPU in browsers, CUDA / Metal
61
+ / Vulkan / DirectML on desktop, ANE / Metal / MLX on iOS, NNAPI / QNN
62
+ Hexagon / GPU delegate on Android.
63
+ - **Multimodal.** Text, image, audio, video — declarative loader for
64
+ cutting-edge models (Gemma 4, LLaVA, Idefics) without waiting for library
65
+ updates.
66
+ - **Distributed inference (v3.0+).** Phone too slow? Offload to your laptop
67
+ on the same Wi-Fi via mDNS pairing — same OpenAI wire, transparent to
68
+ your code. Internet path via a self-hostable rendezvous server.
69
+ - **DVAI Hub (v3.1+).** A first-party desktop utility that turns any device
70
+ into a strong-peer for the rest of your fleet. Brand-neutral install via
71
+ Homebrew / winget / GitHub Releases, OR fork it for your own branded
72
+ companion. Routes through Ollama / LM Studio / vLLM / llama-server /
73
+ llamafile if you've already got those running.
74
+ - **Zero user install.** It's a library, not a daemon. `npm install`,
75
+ `cocoapods`, gradle — your CI already has the muscle for it.
76
+
77
+ ---
78
+
79
+ ## Supported platforms
80
+
81
+ | Stack | Package | Backends |
82
+ | --- | --- | --- |
83
+ | Browser (React, Vue, Svelte, vanilla JS) | `@dvai-bridge/core` + `react` / `vanilla` | WebLLM (WebGPU), Transformers.js (WebGPU / WASM SIMD) |
84
+ | Node / Bun / Electron | `@dvai-bridge/core` | Transformers.js, native llama.cpp |
85
+ | Capacitor hybrid mobile | `@dvai-bridge/capacitor` + backend slice | Native llama.cpp (Metal iOS, Vulkan / CPU Android) |
86
+ | iOS native (Swift) | `DVAIBridge` (SPM / CocoaPods) | llama.cpp (Metal), CoreML / ANE, Apple Foundation Models, MLX |
87
+ | Android native (Kotlin / Java) | `co.deepvoiceai:dvai-bridge` (AAR) | llama.cpp, MediaPipe LLM, LiteRT, NNAPI / QNN |
88
+ | React Native (≥0.77, TurboModule) | `@dvai-bridge/react-native` | All iOS + Android backends (delegates) |
89
+ | Flutter (≥3.39) | `dvai_bridge` (pub.dev) | All iOS + Android backends (Pigeon channels) |
90
+ | .NET 10 LTS (MAUI / Avalonia / WinUI / Catalyst / desktop) | `co.deepvoiceai.dvai-bridge*` (NuGet) | iOS / Android delegate to native; desktop = llama.cpp + ONNX Runtime GenAI + ML.NET |
91
+
92
+ Full quickstart per platform: [dvai-bridge.deepvoiceai.co/guide/getting-started](https://dvai-bridge.deepvoiceai.co/guide/getting-started)
93
+
94
+ ---
95
+
96
+ ## Examples
97
+
98
+ ```ts
99
+ // React
100
+ import { DVAIProvider, useDVAI } from "@dvai-bridge/react";
101
+ <DVAIProvider config={{ backend: "transformers" }}>
102
+ <Chat />
103
+ </DVAIProvider>;
104
+ function Chat() {
105
+ const { isReady, baseUrl } = useDVAI();
106
+ return isReady ? <div>Local AI live at {baseUrl}</div> : <Loading />;
107
+ }
108
+ ```
109
+
110
+ ```swift
111
+ // iOS
112
+ let server = try await DVAIBridge.shared.start()
113
+ // server.baseUrl = "http://127.0.0.1:38883/v1"
114
+ ```
115
+
116
+ ```kotlin
117
+ // Android
118
+ val server = DVAIBridge.start(context)
119
+ // server.baseUrl = "http://127.0.0.1:38883/v1"
120
+ ```
121
+
122
+ ```dart
123
+ // Flutter
124
+ final state = await DVAIBridge.instance.start(
125
+ backend: BackendKind.auto,
126
+ modelPath: '/path/to/model.gguf',
127
+ );
128
+ // state.baseUrl = "http://127.0.0.1:38883/v1"
129
+ ```
130
+
131
+ ```csharp
132
+ // .NET
133
+ var server = await DVAIBridge.Shared.StartAsync(new StartOptions {
134
+ Backend = BackendKind.Auto,
135
+ ModelPath = "/path/to/model.gguf",
136
+ });
137
+ // server.BaseUrl = "http://127.0.0.1:38883/v1"
138
+ ```
139
+
140
+ Multimodal, streaming, embeddings, distributed offload, the Hub —
141
+ everything's at the [docs site](https://dvai-bridge.deepvoiceai.co).
142
+
143
+ ---
144
+
145
+ ## What's new in v3.1
146
+
147
+ - **DVAI Hub** — Tauri desktop utility that's the strong-peer side of v3
148
+ distributed inference. `brew install deepvoiceai/dvai-hub/dvai-hub` (or
149
+ `winget install DeepVoiceAI.DVAIHub`) → mobile apps on the same Wi-Fi
150
+ pair with it and offload heavy inference. [Guide →](https://dvai-bridge.deepvoiceai.co/guide/dvai-hub)
151
+ - **External-engine bridge.** Hub surfaces Ollama / LM Studio / vLLM /
152
+ llama-server / llamafile as additional backend pools so paired apps
153
+ serve from whatever's already cached. Opt-in per engine.
154
+ - **Strict substitution policy.** Models with mismatched family / version /
155
+ size / type are refused by default; quant-only mismatches gated behind a
156
+ per-pairing `preferBetterQuant` flag. No silent mis-routing.
157
+ - **HMAC-signed identity** on `/v1/chat/completions`. Per-app audit logs
158
+ surface who served what, with structured `(appId, peerDeviceId,
159
+ engine, requestedModel, servedModel, outcome)` rows.
160
+ - **Library finalization.** `httpBindHost` (LAN bind), `chatCompletionInterceptor`
161
+ (extension point), HMAC primitives re-exported, `/v1/dvai/*` routes
162
+ actually dispatched, TransformersBackend Node-mode device fix.
163
+ [Migration v3.0 → v3.1 →](https://dvai-bridge.deepvoiceai.co/migration/v3.0-to-v3.1)
164
+
165
+ ---
166
+
167
+ ## Robustness
168
+
169
+ Streaming-correct (SSE passthrough + blank-chunk detection), generation
170
+ timeout, automatic engine-state recovery on fatal errors, port fallback,
171
+ worker offloading, Private Network Access ready, CORS configured. The
172
+ boring substrate so your agent code never has to think about it.
173
+
174
+ ---
175
+
176
+ ## Licensing
177
+
178
+ Dual: **free for development & personal use** on `localhost` (verified at
179
+ runtime). **Commercial use** requires a license key — `info@deepvoiceai.co`.
180
+
181
+ ---
182
+
183
+ ## Contributing
184
+
185
+ PRs welcome.
186
+
187
+ ```bash
188
+ pnpm install
189
+ pnpm build
190
+ bash scripts/build-all.sh # full matrix (auto-skips per-host)
191
+ ```
192
+
193
+ [`CONTRIBUTING.md`](./CONTRIBUTING.md) for the PR flow. Per-platform
194
+ contributor docs (iOS / Android / RN / Flutter / .NET) under
195
+ [`docs/development/`](./docs/development/).
196
+
197
+ ---
198
+
199
+ © Deep Voice AI Limited. All rights reserved.
@@ -0,0 +1,165 @@
1
+ // Phase 3D umbrella module — co.deepvoiceai:dvai-bridge AAR.
2
+ //
3
+ // Depends on the four Android cores (shared / llama / mediapipe / litert)
4
+ // via Maven coordinates (mavenLocal in dev, GitHub Packages in prod). The
5
+ // public DVAIBridge Kotlin object wires those backends behind a single
6
+ // 8-method API surface that mirrors the iOS DVAIBridge SDK.
7
+
8
+ buildscript {
9
+ ext {
10
+ kotlinVersion = '2.3.21'
11
+ }
12
+ repositories {
13
+ google()
14
+ mavenCentral()
15
+ }
16
+ dependencies {
17
+ classpath 'com.android.tools.build:gradle:9.2.0'
18
+ classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion"
19
+ }
20
+ }
21
+
22
+ ext {
23
+ junitVersion = '4.13.2'
24
+ androidxAppCompatVersion = '1.7.1'
25
+ coroutinesVersion = '1.10.2'
26
+ ktorVersion = '2.3.13'
27
+ }
28
+
29
+ allprojects {
30
+ repositories {
31
+ google()
32
+ mavenCentral()
33
+ // Phase 3D: cores resolve here in dev (after `bash scripts/android-publish-local.sh`).
34
+ // Production publishes resolve from GitHub Packages.
35
+ mavenLocal()
36
+ }
37
+ }
38
+
39
+ apply plugin: 'com.android.library'
40
+ apply plugin: 'maven-publish'
41
+
42
+ android {
43
+ namespace 'co.deepvoiceai.bridge'
44
+ // Phase 2 Task 3 (examples) library fix: see shared-core/android/build.gradle
45
+ // for the rationale — `-PcompileSdkOverride=35` lets Windows hosts sidestep
46
+ // an AGP 9.2.0 parseLocalResources bug against android-36's public-final.xml.
47
+ compileSdk(project.findProperty('compileSdkOverride')?.toInteger() ?: 36)
48
+
49
+ defaultConfig {
50
+ minSdk 24
51
+ targetSdk(project.findProperty('compileSdkOverride')?.toInteger() ?: 36)
52
+ testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
53
+
54
+ // Phase 3D Task 17: integration tests read SMOKE_* env vars via
55
+ // BuildConfig fields. Empty defaults make the tests self-skip via
56
+ // `Assume.assumeFalse(... isEmpty())` when the env vars aren't set.
57
+ // CI workflows export the env vars from repo secrets; local dev
58
+ // can `export SMOKE_MODEL_URL=... && ./gradlew connectedAndroidTest`.
59
+ buildConfigField "String", "SMOKE_MODEL_URL",
60
+ "\"${System.getenv('SMOKE_MODEL_URL') ?: ''}\""
61
+ buildConfigField "String", "SMOKE_MODEL_SHA256",
62
+ "\"${System.getenv('SMOKE_MODEL_SHA256') ?: ''}\""
63
+ buildConfigField "String", "SMOKE_MEDIAPIPE_MODEL_URL",
64
+ "\"${System.getenv('SMOKE_MEDIAPIPE_MODEL_URL') ?: ''}\""
65
+ buildConfigField "String", "SMOKE_MEDIAPIPE_MODEL_SHA256",
66
+ "\"${System.getenv('SMOKE_MEDIAPIPE_MODEL_SHA256') ?: ''}\""
67
+ buildConfigField "String", "SMOKE_LITERT_MODEL_URL",
68
+ "\"${System.getenv('SMOKE_LITERT_MODEL_URL') ?: ''}\""
69
+ buildConfigField "String", "SMOKE_LITERT_MODEL_SHA256",
70
+ "\"${System.getenv('SMOKE_LITERT_MODEL_SHA256') ?: ''}\""
71
+ buildConfigField "String", "SMOKE_LITERT_TOKENIZER_URL",
72
+ "\"${System.getenv('SMOKE_LITERT_TOKENIZER_URL') ?: ''}\""
73
+ }
74
+
75
+ buildFeatures {
76
+ buildConfig = true
77
+ }
78
+
79
+ compileOptions {
80
+ sourceCompatibility JavaVersion.VERSION_17
81
+ targetCompatibility JavaVersion.VERSION_17
82
+ }
83
+ testOptions {
84
+ unitTests.includeAndroidResources = true
85
+ }
86
+
87
+ publishing {
88
+ singleVariant('release') {
89
+ withSourcesJar()
90
+ }
91
+ }
92
+ }
93
+
94
+ kotlin {
95
+ compilerOptions {
96
+ jvmTarget = org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_17
97
+ }
98
+ }
99
+
100
+ dependencies {
101
+ // Cores — each declared as `api` so consumer apps that pull just the
102
+ // umbrella see the underlying types (BackendKind, HandlerContext, etc.)
103
+ // without re-declaring the deps.
104
+ api "co.deepvoiceai:android-shared-core:$dvaiBridgeVersion"
105
+ api "co.deepvoiceai:android-llama-core:$dvaiBridgeVersion"
106
+ api "co.deepvoiceai:android-mediapipe-core:$dvaiBridgeVersion"
107
+ api "co.deepvoiceai:android-litert-core:$dvaiBridgeVersion"
108
+
109
+ implementation "androidx.appcompat:appcompat:$androidxAppCompatVersion"
110
+ implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutinesVersion"
111
+ implementation 'org.jetbrains.kotlinx:kotlinx-serialization-json:1.11.0'
112
+
113
+ // v3.3 — offline JWT license validator. nimbus-jose-jwt is the
114
+ // widely-vetted JOSE implementation for the JVM; we use ES256 only
115
+ // (refuse alg=none / HS256, classic algorithm-confusion defense).
116
+ // See android/src/main/java/co/deepvoiceai/bridge/license/.
117
+ implementation 'com.nimbusds:nimbus-jose-jwt:10.5'
118
+
119
+ // Phase 5 (v3.2) — outgoing-offload pre-routing proxy. The proxy
120
+ // binds the public port; the underlying native backend binds an
121
+ // internal loopback port. Per-request the proxy decides local-vs-
122
+ // forward and either streams from local or HMAC-signs + forwards
123
+ // to a paired peer. CIO engine is the smallest of the Ktor engine
124
+ // options (~500 KB AAR contribution vs Netty's ~3 MB).
125
+ implementation "io.ktor:ktor-server-core:$ktorVersion"
126
+ implementation "io.ktor:ktor-server-cio:$ktorVersion"
127
+ implementation "io.ktor:ktor-server-status-pages:$ktorVersion"
128
+ implementation "io.ktor:ktor-client-core:$ktorVersion"
129
+ implementation "io.ktor:ktor-client-cio:$ktorVersion"
130
+
131
+ testImplementation "junit:junit:$junitVersion"
132
+ testImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:$coroutinesVersion"
133
+ testImplementation 'org.robolectric:robolectric:4.16.1'
134
+ testImplementation "io.ktor:ktor-server-test-host:$ktorVersion"
135
+ testImplementation 'com.squareup.okhttp3:mockwebserver:5.3.2'
136
+
137
+ androidTestImplementation 'androidx.test:runner:1.7.0'
138
+ androidTestImplementation 'androidx.test.ext:junit:1.3.0'
139
+ androidTestImplementation 'com.squareup.okhttp3:okhttp:5.3.2'
140
+ }
141
+
142
+ // Phase 3D Task 4: copy-paste publishing block (see llama-core for the
143
+ // rationale on why each Gradle root has its own copy).
144
+ afterEvaluate {
145
+ publishing {
146
+ publications {
147
+ release(MavenPublication) {
148
+ groupId = 'co.deepvoiceai'
149
+ artifactId = 'dvai-bridge'
150
+ version = (project.findProperty('dvaiBridgeVersion') ?: '4.0.0').toString()
151
+ from components.release
152
+ }
153
+ }
154
+ repositories {
155
+ maven {
156
+ name = 'GitHubPackages'
157
+ url = uri('https://maven.pkg.github.com/dvai-global/dvai-bridge')
158
+ credentials {
159
+ username = project.findProperty('gpr.user') ?: System.getenv('GITHUB_ACTOR')
160
+ password = project.findProperty('gpr.key') ?: System.getenv('GITHUB_TOKEN')
161
+ }
162
+ }
163
+ }
164
+ }
165
+ }
@@ -0,0 +1,5 @@
1
+ android.useAndroidX=true
2
+ kotlin.code.style=official
3
+ android.nonTransitiveRClass=true
4
+ org.gradle.jvmargs=-Xmx4096m
5
+ dvaiBridgeVersion=4.0.0
@@ -0,0 +1 @@
1
+ rootProject.name = 'dvai-bridge-android'
@@ -0,0 +1,162 @@
1
+ package co.deepvoiceai.bridge
2
+
3
+ import androidx.test.platform.app.InstrumentationRegistry
4
+ import androidx.test.ext.junit.runners.AndroidJUnit4
5
+ import okhttp3.MediaType.Companion.toMediaType
6
+ import okhttp3.OkHttpClient
7
+ import okhttp3.Request
8
+ import okhttp3.RequestBody.Companion.toRequestBody
9
+ import org.json.JSONArray
10
+ import org.json.JSONObject
11
+ import org.junit.After
12
+ import org.junit.Assert.assertFalse
13
+ import org.junit.Assert.assertNotNull
14
+ import org.junit.Assume.assumeFalse
15
+ import org.junit.Test
16
+ import org.junit.runner.RunWith
17
+ import kotlinx.coroutines.runBlocking
18
+ import java.io.File
19
+ import java.security.MessageDigest
20
+
21
+ /**
22
+ * End-to-end integration tests for the Android Native SDK against real models.
23
+ * Each backend has its own test method; each skips cleanly when its
24
+ * prereqs aren't met (env vars / BuildConfig fields missing).
25
+ *
26
+ * Mirrors the iOS `RealModelIntegrationTest.swift` pattern.
27
+ *
28
+ * Set env vars on the host (read by Gradle into BuildConfig via Task 17's
29
+ * `buildConfigField` injection):
30
+ * - SMOKE_MODEL_URL + SMOKE_MODEL_SHA256 (Llama)
31
+ * - SMOKE_MEDIAPIPE_MODEL_URL + SMOKE_MEDIAPIPE_MODEL_SHA256
32
+ * - SMOKE_LITERT_MODEL_URL + SMOKE_LITERT_MODEL_SHA256 + SMOKE_LITERT_TOKENIZER_URL
33
+ *
34
+ * Run: `./gradlew :dvai-bridge-android:connectedAndroidTest` against a
35
+ * connected device or emulator.
36
+ */
37
+ @RunWith(AndroidJUnit4::class)
38
+ class RealModelIntegrationTest {
39
+ private val context get() = InstrumentationRegistry.getInstrumentation().targetContext
40
+ private val httpClient = OkHttpClient()
41
+
42
+ @After
43
+ fun tearDown() = runBlocking {
44
+ runCatching { DVAIBridge.stop() }
45
+ }
46
+
47
+ @Test
48
+ fun llamaBackendIntegration() = runBlocking {
49
+ val url = BuildConfig.SMOKE_MODEL_URL
50
+ val sha = BuildConfig.SMOKE_MODEL_SHA256
51
+ assumeFalse("SMOKE_MODEL_URL not set", url.isEmpty())
52
+ assumeFalse("SMOKE_MODEL_SHA256 not set", sha.isEmpty())
53
+
54
+ DVAIBridge.init(context)
55
+ val download = DVAIBridge.downloadModel(
56
+ DownloadOptions(url = url, sha256 = sha, destFilename = "int-llama.gguf"),
57
+ )
58
+
59
+ val server = DVAIBridge.start(StartOptions(
60
+ backend = BackendKind.Llama,
61
+ modelPath = download.path,
62
+ // GPU offload off by default on emulator (no Vulkan).
63
+ gpuLayers = 0,
64
+ contextSize = 1024,
65
+ ))
66
+ assertEquals(BackendKind.Llama, server.backend)
67
+
68
+ val response = postChatCompletion(server.baseUrl, "What is 2+2?")
69
+ assertFalse("llama completion should not be empty", response.isEmpty())
70
+ }
71
+
72
+ @Test
73
+ fun mediaPipeBackendIntegration() = runBlocking {
74
+ val url = BuildConfig.SMOKE_MEDIAPIPE_MODEL_URL
75
+ val sha = BuildConfig.SMOKE_MEDIAPIPE_MODEL_SHA256
76
+ assumeFalse("SMOKE_MEDIAPIPE_MODEL_URL not set", url.isEmpty())
77
+ assumeFalse("SMOKE_MEDIAPIPE_MODEL_SHA256 not set", sha.isEmpty())
78
+
79
+ DVAIBridge.init(context)
80
+ val download = DVAIBridge.downloadModel(
81
+ DownloadOptions(url = url, sha256 = sha, destFilename = "int-mediapipe.task"),
82
+ )
83
+
84
+ val server = DVAIBridge.start(StartOptions(
85
+ backend = BackendKind.MediaPipe,
86
+ modelPath = download.path,
87
+ ))
88
+ assertEquals(BackendKind.MediaPipe, server.backend)
89
+
90
+ val response = postChatCompletion(server.baseUrl, "Hello")
91
+ assertFalse("mediapipe completion should not be empty", response.isEmpty())
92
+ }
93
+
94
+ @Test
95
+ fun liteRTBackendIntegration() = runBlocking {
96
+ val modelUrl = BuildConfig.SMOKE_LITERT_MODEL_URL
97
+ val modelSha = BuildConfig.SMOKE_LITERT_MODEL_SHA256
98
+ val tokUrl = BuildConfig.SMOKE_LITERT_TOKENIZER_URL
99
+ assumeFalse("SMOKE_LITERT_MODEL_URL not set", modelUrl.isEmpty())
100
+ assumeFalse("SMOKE_LITERT_MODEL_SHA256 not set", modelSha.isEmpty())
101
+ assumeFalse("SMOKE_LITERT_TOKENIZER_URL not set", tokUrl.isEmpty())
102
+
103
+ DVAIBridge.init(context)
104
+ val modelDownload = DVAIBridge.downloadModel(
105
+ DownloadOptions(url = modelUrl, sha256 = modelSha, destFilename = "int-litert.tflite"),
106
+ )
107
+ // tokenizer.json: download into the same cache dir, no sha required (small file).
108
+ val tokDir = File(modelDownload.path).parentFile ?: error("download path has no parent")
109
+ val tokFile = File(tokDir, "tokenizer.json")
110
+ downloadRaw(tokUrl, tokFile)
111
+
112
+ val server = DVAIBridge.start(StartOptions(
113
+ backend = BackendKind.LiteRT,
114
+ modelPath = modelDownload.path,
115
+ tokenizerPath = tokDir.absolutePath,
116
+ maxNewTokens = 32,
117
+ ))
118
+ assertEquals(BackendKind.LiteRT, server.backend)
119
+
120
+ val response = postChatCompletion(server.baseUrl, "What is 2+2?")
121
+ assertFalse("litert completion should not be empty", response.isEmpty())
122
+ }
123
+
124
+ private fun postChatCompletion(baseUrl: String, userMessage: String): String {
125
+ val body = JSONObject().apply {
126
+ put("messages", JSONArray().apply {
127
+ put(JSONObject().apply {
128
+ put("role", "user")
129
+ put("content", userMessage)
130
+ })
131
+ })
132
+ put("max_tokens", 32)
133
+ put("temperature", 0.0)
134
+ }.toString()
135
+ val req = Request.Builder()
136
+ .url("$baseUrl/chat/completions")
137
+ .post(body.toRequestBody("application/json".toMediaType()))
138
+ .build()
139
+ httpClient.newCall(req).execute().use { resp ->
140
+ val text = resp.body?.string() ?: ""
141
+ assertNotNull("response body", text)
142
+ if (resp.code != 200) error("POST failed: ${resp.code} $text")
143
+ val json = JSONObject(text)
144
+ val choices = json.optJSONArray("choices") ?: return ""
145
+ if (choices.length() == 0) return ""
146
+ val message = choices.getJSONObject(0).optJSONObject("message") ?: return ""
147
+ return message.optString("content", "")
148
+ }
149
+ }
150
+
151
+ private fun downloadRaw(url: String, dest: File) {
152
+ val req = Request.Builder().url(url).build()
153
+ httpClient.newCall(req).execute().use { resp ->
154
+ if (resp.code != 200) error("downloadRaw($url) -> ${resp.code}")
155
+ dest.outputStream().use { out -> resp.body!!.byteStream().copyTo(out) }
156
+ }
157
+ }
158
+
159
+ private fun assertEquals(expected: BackendKind, actual: BackendKind) {
160
+ if (expected != actual) error("expected $expected, got $actual")
161
+ }
162
+ }
@@ -0,0 +1,3 @@
1
+ <?xml version="1.0" encoding="utf-8"?>
2
+ <manifest xmlns:android="http://schemas.android.com/apk/res/android">
3
+ </manifest>
@@ -0,0 +1,21 @@
1
+ package co.deepvoiceai.bridge
2
+
3
+ /**
4
+ * Inference backend selector. Mirrors the iOS DVAIBridge `BackendKind`
5
+ * enum 1:1 in name and case order so cross-platform consumers can
6
+ * reason about them identically.
7
+ *
8
+ * - [Auto]: Pick the best backend for the supplied options at
9
+ * runtime. See [BackendSelector] for the resolution rules.
10
+ * - [Llama]: llama.cpp via android-llama-core. Universal `.gguf` support.
11
+ * - [MediaPipe]: Google's MediaPipe LLM Inference (LiteRT-LM under the hood
12
+ * post-Phase 3B). Consumes `.task` checkpoints.
13
+ * - [LiteRT]: Bare LiteRT (TFLite successor) for `.tflite` / `.litertlm`
14
+ * Llama-style stateful checkpoints — Phase 3D's new backend.
15
+ */
16
+ enum class BackendKind {
17
+ Auto,
18
+ Llama,
19
+ MediaPipe,
20
+ LiteRT,
21
+ }
@@ -0,0 +1,28 @@
1
+ package co.deepvoiceai.bridge
2
+
3
+ import java.io.File
4
+
5
+ /**
6
+ * Auto-resolution rules for [BackendKind.Auto]. Pure function, no Android
7
+ * runtime dependency — testable as a JVM unit test.
8
+ *
9
+ * The order below is the resolution priority. First matching rule wins:
10
+ * 1. modelPath ends with `.task` and the file exists -> MediaPipe.
11
+ * 2. modelPath ends with `.tflite` or `.litertlm` -> LiteRT.
12
+ * 3. Default (incl. .gguf and any unknown extension) -> Llama.
13
+ *
14
+ * Mirrors the iOS DVAIBridge BackendSelector logic (see
15
+ * `packages/dvai-bridge-ios/ios/Sources/DVAIBridge/BackendSelector.swift`)
16
+ * — keep them in lockstep when adding new dispatch heuristics.
17
+ */
18
+ object BackendSelector {
19
+ fun resolve(opts: StartOptions): BackendKind {
20
+ if (opts.backend != BackendKind.Auto) return opts.backend
21
+ val path = opts.modelPath
22
+ if (path != null) {
23
+ if (path.endsWith(".task") && File(path).exists()) return BackendKind.MediaPipe
24
+ if (path.endsWith(".tflite") || path.endsWith(".litertlm")) return BackendKind.LiteRT
25
+ }
26
+ return BackendKind.Llama
27
+ }
28
+ }