@dvai-bridge/react-native 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.
- package/LICENSE +51 -0
- package/README.md +199 -0
- package/android/build.gradle +132 -0
- package/android/gradle.properties +7 -0
- package/android/settings.gradle +1 -0
- package/android/src/main/AndroidManifest.xml +3 -0
- package/android/src/main/java/co/deepvoiceai/bridge/rn/DVAIBridgeNativeModuleImpl.kt +443 -0
- package/android/src/main/java/co/deepvoiceai/bridge/rn/DVAIBridgePackage.kt +42 -0
- package/android/src/newarch/java/co/deepvoiceai/bridge/rn/DVAIBridgeNativeModule.kt +64 -0
- package/android/src/oldarch/java/co/deepvoiceai/bridge/rn/DVAIBridgeNativeModule.kt +70 -0
- package/package.json +127 -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
|
+

|
|
2
|
+
|
|
3
|
+
# DVAI-Bridge
|
|
4
|
+
|
|
5
|
+
<!-- [](https://github.com/Westenets/dvai-bridge/actions/workflows/smoke-real-models.yml) -->
|
|
6
|
+
|
|
7
|
+
[](LICENSE)      
|
|
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,132 @@
|
|
|
1
|
+
// React Native TurboModule bridge for the DVAIBridge Android SDK
|
|
2
|
+
// (Phase 3D umbrella, `co.deepvoiceai:dvai-bridge:2.1.0`).
|
|
3
|
+
//
|
|
4
|
+
// This module is **thin**: no inference engine, no model downloader, no HTTP
|
|
5
|
+
// server. It depends on the umbrella AAR (declared below) and exposes its
|
|
6
|
+
// `DVAIBridge` Kotlin singleton to React Native via a TurboModule.
|
|
7
|
+
//
|
|
8
|
+
// Consumers wiring this into their RN app need three things in their Android
|
|
9
|
+
// project:
|
|
10
|
+
//
|
|
11
|
+
// 1. `mavenCentral()` and `google()` repositories (default RN template).
|
|
12
|
+
// 2. A GitHub Packages Maven entry pointing at
|
|
13
|
+
// `https://maven.pkg.github.com/dvai-global/dvai-bridge` plus a
|
|
14
|
+
// `gpr.user` / `gpr.key` pair in `~/.gradle/gradle.properties` (or
|
|
15
|
+
// `GITHUB_ACTOR` / `GITHUB_TOKEN` env vars in CI). See the
|
|
16
|
+
// docs/guide/react-native-sdk.md guide for the snippet.
|
|
17
|
+
// 3. `applicationContext` initialization in `Application.onCreate()`:
|
|
18
|
+
//
|
|
19
|
+
// DVAIBridge.init(this)
|
|
20
|
+
//
|
|
21
|
+
// The bridge re-runs `DVAIBridge.init(reactContext.applicationContext)`
|
|
22
|
+
// defensively on every TurboModule construction, so this step is
|
|
23
|
+
// strictly optional unless your consumer app needs Compose / Lifecycle
|
|
24
|
+
// observers wired up before the first JS-side `start()` call.
|
|
25
|
+
|
|
26
|
+
buildscript {
|
|
27
|
+
ext {
|
|
28
|
+
kotlinVersion = project.findProperty('DVAIBridge_kotlinVersion') ?: '2.3.21'
|
|
29
|
+
agpVersion = project.findProperty('DVAIBridge_agpVersion') ?: '9.2.0'
|
|
30
|
+
}
|
|
31
|
+
repositories {
|
|
32
|
+
google()
|
|
33
|
+
mavenCentral()
|
|
34
|
+
}
|
|
35
|
+
dependencies {
|
|
36
|
+
classpath "com.android.tools.build:gradle:${agpVersion}"
|
|
37
|
+
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:${kotlinVersion}"
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
apply plugin: 'com.android.library'
|
|
42
|
+
apply plugin: 'org.jetbrains.kotlin.android'
|
|
43
|
+
|
|
44
|
+
def safeExtGet(prop, fallback) {
|
|
45
|
+
rootProject.ext.has(prop) ? rootProject.ext.get(prop) : fallback
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
def reactNativeArchitectures() {
|
|
49
|
+
def value = rootProject.getProperties().get("reactNativeArchitectures")
|
|
50
|
+
return value ? value.split(",") : ["armeabi-v7a", "x86", "x86_64", "arm64-v8a"]
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
def isNewArchitectureEnabled() {
|
|
54
|
+
return rootProject.hasProperty("newArchEnabled") &&
|
|
55
|
+
rootProject.getProperty("newArchEnabled") == "true"
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
android {
|
|
59
|
+
namespace "co.deepvoiceai.bridge.rn"
|
|
60
|
+
|
|
61
|
+
compileSdkVersion safeExtGet("compileSdkVersion", 36)
|
|
62
|
+
buildToolsVersion safeExtGet("buildToolsVersion", "36.0.0")
|
|
63
|
+
|
|
64
|
+
defaultConfig {
|
|
65
|
+
minSdkVersion safeExtGet("minSdkVersion", 24)
|
|
66
|
+
targetSdkVersion safeExtGet("targetSdkVersion", 36)
|
|
67
|
+
|
|
68
|
+
// Codegen jsRootDir + libraryName align with package.json's
|
|
69
|
+
// codegenConfig block (jsSrcsDir + name).
|
|
70
|
+
buildConfigField "boolean", "IS_NEW_ARCHITECTURE_ENABLED", "${isNewArchitectureEnabled()}"
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
buildFeatures {
|
|
74
|
+
buildConfig true
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
compileOptions {
|
|
78
|
+
sourceCompatibility JavaVersion.VERSION_17
|
|
79
|
+
targetCompatibility JavaVersion.VERSION_17
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
sourceSets {
|
|
83
|
+
main {
|
|
84
|
+
if (isNewArchitectureEnabled()) {
|
|
85
|
+
java.srcDirs += [
|
|
86
|
+
"src/newarch",
|
|
87
|
+
// Codegen output dir injected by RN at sync time.
|
|
88
|
+
"${project.buildDir}/generated/source/codegen/java",
|
|
89
|
+
]
|
|
90
|
+
} else {
|
|
91
|
+
java.srcDirs += ["src/oldarch"]
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
kotlin {
|
|
98
|
+
jvmToolchain(17)
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
repositories {
|
|
102
|
+
mavenCentral()
|
|
103
|
+
google()
|
|
104
|
+
// Phase 3D umbrella + cores resolve here in dev (after running
|
|
105
|
+
// `bash scripts/android-publish-local.sh` locally).
|
|
106
|
+
mavenLocal()
|
|
107
|
+
maven {
|
|
108
|
+
// Production GitHub Packages Maven for the umbrella + cores. Picked up
|
|
109
|
+
// automatically when the consumer app exports `gpr.user` / `gpr.key`
|
|
110
|
+
// (or sets the matching env vars in CI).
|
|
111
|
+
url = uri("https://maven.pkg.github.com/dvai-global/dvai-bridge")
|
|
112
|
+
credentials {
|
|
113
|
+
username = project.findProperty("gpr.user") ?: System.getenv("GITHUB_ACTOR")
|
|
114
|
+
password = project.findProperty("gpr.key") ?: System.getenv("GITHUB_TOKEN")
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
def dvaiBridgeVersion = project.findProperty('dvaiBridgeVersion') ?: '4.0.0'
|
|
120
|
+
|
|
121
|
+
dependencies {
|
|
122
|
+
implementation "com.facebook.react:react-android"
|
|
123
|
+
|
|
124
|
+
// Phase 3D umbrella AAR — provides DVAIBridge, BackendKind, StartOptions,
|
|
125
|
+
// BoundServer, ProgressEvent, DVAIBridgeError. `api` instead of
|
|
126
|
+
// `implementation` so consumer apps that need to catch DVAIBridgeError
|
|
127
|
+
// subtypes (or pattern-match on BackendKind) see the symbols without
|
|
128
|
+
// adding a redundant top-level dependency.
|
|
129
|
+
api "co.deepvoiceai:dvai-bridge:${dvaiBridgeVersion}"
|
|
130
|
+
|
|
131
|
+
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.10.2"
|
|
132
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
rootProject.name = 'dvai-bridge-react-native'
|
|
@@ -0,0 +1,443 @@
|
|
|
1
|
+
package co.deepvoiceai.bridge.rn
|
|
2
|
+
|
|
3
|
+
import co.deepvoiceai.bridge.BackendKind
|
|
4
|
+
import co.deepvoiceai.bridge.DVAIBridge
|
|
5
|
+
import co.deepvoiceai.bridge.DVAIBridgeError
|
|
6
|
+
import co.deepvoiceai.bridge.DownloadOptions
|
|
7
|
+
import co.deepvoiceai.bridge.ProgressEvent
|
|
8
|
+
import co.deepvoiceai.bridge.StartOptions
|
|
9
|
+
import co.deepvoiceai.bridge.shared.core.CorsConfig
|
|
10
|
+
import com.facebook.react.bridge.Arguments
|
|
11
|
+
import com.facebook.react.bridge.Promise
|
|
12
|
+
import com.facebook.react.bridge.ReactApplicationContext
|
|
13
|
+
import com.facebook.react.bridge.ReadableArray
|
|
14
|
+
import com.facebook.react.bridge.ReadableMap
|
|
15
|
+
import com.facebook.react.bridge.WritableMap
|
|
16
|
+
import com.facebook.react.modules.core.DeviceEventManagerModule
|
|
17
|
+
import kotlinx.coroutines.CoroutineScope
|
|
18
|
+
import kotlinx.coroutines.Dispatchers
|
|
19
|
+
import kotlinx.coroutines.Job
|
|
20
|
+
import kotlinx.coroutines.SupervisorJob
|
|
21
|
+
import kotlinx.coroutines.cancel
|
|
22
|
+
import kotlinx.coroutines.flow.collect
|
|
23
|
+
import kotlinx.coroutines.launch
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Shared implementation of the React Native TurboModule bridge for the
|
|
27
|
+
* DVAIBridge Android SDK (Phase 3D umbrella).
|
|
28
|
+
*
|
|
29
|
+
* This class is `internal` and called from the platform-specific module
|
|
30
|
+
* shims under `src/newarch/` (TurboModule subclass) and `src/oldarch/`
|
|
31
|
+
* (legacy `ReactContextBaseJavaModule` subclass). Both shims forward into
|
|
32
|
+
* a single instance of this class so the Kotlin business logic isn't
|
|
33
|
+
* duplicated.
|
|
34
|
+
*
|
|
35
|
+
* The bridge:
|
|
36
|
+
*
|
|
37
|
+
* 1. Calls `DVAIBridge.init(applicationContext)` once on first construction
|
|
38
|
+
* so consumers don't have to remember to do it from their `Application`.
|
|
39
|
+
* 2. Translates `ReadableMap` opts ↔ `StartOptions` / `DownloadOptions`.
|
|
40
|
+
* 3. Wraps each `DVAIBridge.start(...)` / `.stop(...)` / etc. call in a
|
|
41
|
+
* coroutine and resolves / rejects the corresponding `Promise`.
|
|
42
|
+
* 4. Collects from `DVAIBridge.progressFlow` and re-emits each event as
|
|
43
|
+
* a `DVAIBridgeProgress` JS event with the canonical JSON shape.
|
|
44
|
+
*
|
|
45
|
+
* The progress collector is started once on first JS-side
|
|
46
|
+
* `addListener("DVAIBridgeProgress", …)` and torn down when listener count
|
|
47
|
+
* drops to zero (mirroring the iOS Combine subscription lifecycle).
|
|
48
|
+
*/
|
|
49
|
+
internal class DVAIBridgeNativeModuleImpl(
|
|
50
|
+
private val reactContext: ReactApplicationContext,
|
|
51
|
+
) {
|
|
52
|
+
/** Coroutine scope tied to the lifetime of the React module instance. */
|
|
53
|
+
private val moduleJob: Job = SupervisorJob()
|
|
54
|
+
private val moduleScope: CoroutineScope = CoroutineScope(Dispatchers.Default + moduleJob)
|
|
55
|
+
|
|
56
|
+
/** Job for the active progress-event collector, if any. */
|
|
57
|
+
private var progressJob: Job? = null
|
|
58
|
+
|
|
59
|
+
/** Listener count tracked by RN's NativeEventEmitter. */
|
|
60
|
+
private var listenerCount: Int = 0
|
|
61
|
+
|
|
62
|
+
init {
|
|
63
|
+
// Defensive init — consumers SHOULD call DVAIBridge.init(this) from
|
|
64
|
+
// their Application.onCreate(), but we don't want a silent
|
|
65
|
+
// ConfigurationInvalid if they forget. The umbrella's init is
|
|
66
|
+
// idempotent so this is safe to re-run.
|
|
67
|
+
DVAIBridge.init(reactContext.applicationContext)
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
fun invalidate() {
|
|
71
|
+
progressJob?.cancel()
|
|
72
|
+
progressJob = null
|
|
73
|
+
moduleScope.cancel()
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// MARK: - Lifecycle methods
|
|
77
|
+
|
|
78
|
+
fun startBridge(opts: ReadableMap, promise: Promise) {
|
|
79
|
+
val startOpts = try {
|
|
80
|
+
parseStartOptions(opts)
|
|
81
|
+
} catch (e: DVAIBridgeError) {
|
|
82
|
+
promise.rejectWith(e)
|
|
83
|
+
return
|
|
84
|
+
} catch (e: Throwable) {
|
|
85
|
+
promise.reject("configurationInvalid", e.message ?: "Failed to parse StartOptions", e)
|
|
86
|
+
return
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
moduleScope.launch {
|
|
90
|
+
try {
|
|
91
|
+
val server = DVAIBridge.start(startOpts)
|
|
92
|
+
val map = Arguments.createMap().apply {
|
|
93
|
+
putString("baseUrl", server.baseUrl)
|
|
94
|
+
putInt("port", server.port)
|
|
95
|
+
putString("backend", server.backend.toJsName())
|
|
96
|
+
putString("modelId", server.modelId)
|
|
97
|
+
}
|
|
98
|
+
promise.resolve(map)
|
|
99
|
+
} catch (e: DVAIBridgeError) {
|
|
100
|
+
promise.rejectWith(e)
|
|
101
|
+
} catch (e: Throwable) {
|
|
102
|
+
promise.reject("backendError", e.message ?: e::class.qualifiedName ?: "unknown", e)
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
fun stopBridge(promise: Promise) {
|
|
108
|
+
moduleScope.launch {
|
|
109
|
+
try {
|
|
110
|
+
DVAIBridge.stop()
|
|
111
|
+
promise.resolve(null)
|
|
112
|
+
} catch (e: DVAIBridgeError) {
|
|
113
|
+
promise.rejectWith(e)
|
|
114
|
+
} catch (e: Throwable) {
|
|
115
|
+
promise.reject("backendError", e.message ?: e::class.qualifiedName ?: "unknown", e)
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
fun status(promise: Promise) {
|
|
121
|
+
val info = DVAIBridge.status()
|
|
122
|
+
val map = Arguments.createMap().apply {
|
|
123
|
+
putBoolean("running", info.running)
|
|
124
|
+
info.baseUrl?.let { putString("baseUrl", it) }
|
|
125
|
+
info.backend?.let { putString("backend", it.toJsName()) }
|
|
126
|
+
info.modelId?.let { putString("modelId", it) }
|
|
127
|
+
// Port from baseUrl, when present. Mirrors the iOS impl.
|
|
128
|
+
info.baseUrl?.let { url ->
|
|
129
|
+
runCatching {
|
|
130
|
+
val parsed = java.net.URI(url)
|
|
131
|
+
if (parsed.port > 0) putInt("port", parsed.port)
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
promise.resolve(map)
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
fun downloadModel(opts: ReadableMap, promise: Promise) {
|
|
139
|
+
val url = opts.getString("url")
|
|
140
|
+
?: return promise.reject("configurationInvalid", "downloadModel: missing `url`", null)
|
|
141
|
+
val sha256 = opts.getString("sha256")
|
|
142
|
+
?: return promise.reject("configurationInvalid", "downloadModel: missing `sha256`", null)
|
|
143
|
+
val destFilename = opts.getString("destFilename")
|
|
144
|
+
?: url.substringAfterLast('/').ifEmpty {
|
|
145
|
+
return promise.reject(
|
|
146
|
+
"configurationInvalid",
|
|
147
|
+
"downloadModel: cannot derive destFilename from URL `$url`; pass it explicitly.",
|
|
148
|
+
null,
|
|
149
|
+
)
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
moduleScope.launch {
|
|
153
|
+
try {
|
|
154
|
+
val result = DVAIBridge.downloadModel(
|
|
155
|
+
DownloadOptions(
|
|
156
|
+
url = url,
|
|
157
|
+
sha256 = sha256,
|
|
158
|
+
destFilename = destFilename,
|
|
159
|
+
),
|
|
160
|
+
)
|
|
161
|
+
val map = Arguments.createMap().apply {
|
|
162
|
+
putString("path", result.path)
|
|
163
|
+
putString("sha256", result.sha256)
|
|
164
|
+
putDouble("sizeBytes", result.sizeBytes.toDouble())
|
|
165
|
+
}
|
|
166
|
+
promise.resolve(map)
|
|
167
|
+
} catch (e: DVAIBridgeError) {
|
|
168
|
+
promise.rejectWith(e)
|
|
169
|
+
} catch (e: Throwable) {
|
|
170
|
+
promise.reject("downloadFailed", e.message ?: e::class.qualifiedName ?: "unknown", e)
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/* ---------------------------------------------------------------- *
|
|
176
|
+
* v3.2 — pre-init hardware assessment *
|
|
177
|
+
* ---------------------------------------------------------------- */
|
|
178
|
+
|
|
179
|
+
fun assessHardware(hardwareMinimum: Double, minLocalCapability: Double, promise: Promise) {
|
|
180
|
+
try {
|
|
181
|
+
val a = DVAIBridge.assessHardware(
|
|
182
|
+
hardwareMinimum = hardwareMinimum,
|
|
183
|
+
minLocalCapability = minLocalCapability,
|
|
184
|
+
)
|
|
185
|
+
val hintsMap = Arguments.createMap().apply {
|
|
186
|
+
putBoolean("hasNpu", a.hints.hasNpu)
|
|
187
|
+
putInt("ramGb", a.hints.ramGb)
|
|
188
|
+
putString("gpuClass", a.hints.gpuClass.toJsName())
|
|
189
|
+
putString("cpuClass", a.hints.cpuClass.toJsName())
|
|
190
|
+
}
|
|
191
|
+
val map = Arguments.createMap().apply {
|
|
192
|
+
putString("mode", a.mode.toJsName())
|
|
193
|
+
putDouble("tokPerSec", a.tokPerSec)
|
|
194
|
+
putString("reason", a.reason)
|
|
195
|
+
putMap("hints", hintsMap)
|
|
196
|
+
}
|
|
197
|
+
promise.resolve(map)
|
|
198
|
+
} catch (e: Throwable) {
|
|
199
|
+
promise.reject(
|
|
200
|
+
"configurationInvalid",
|
|
201
|
+
e.message ?: e::class.qualifiedName ?: "assessHardware failed",
|
|
202
|
+
e,
|
|
203
|
+
)
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// MARK: - Event emitter housekeeping
|
|
208
|
+
|
|
209
|
+
fun addListener(@Suppress("UNUSED_PARAMETER") eventName: String) {
|
|
210
|
+
if (listenerCount == 0) {
|
|
211
|
+
attachProgressCollector()
|
|
212
|
+
}
|
|
213
|
+
listenerCount += 1
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
fun removeListeners(count: Int) {
|
|
217
|
+
listenerCount = (listenerCount - count).coerceAtLeast(0)
|
|
218
|
+
if (listenerCount == 0) {
|
|
219
|
+
progressJob?.cancel()
|
|
220
|
+
progressJob = null
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
private fun attachProgressCollector() {
|
|
225
|
+
progressJob?.cancel()
|
|
226
|
+
progressJob = moduleScope.launch {
|
|
227
|
+
DVAIBridge.progressFlow.collect { event ->
|
|
228
|
+
val payload = event.toJsEvent()
|
|
229
|
+
emitProgressEvent(payload)
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
private fun emitProgressEvent(payload: WritableMap) {
|
|
235
|
+
// Use the bridge's DeviceEventManagerModule.RCTDeviceEventEmitter to
|
|
236
|
+
// dispatch events. Compatible with both old-arch and new-arch
|
|
237
|
+
// (Bridgeless) RN.
|
|
238
|
+
try {
|
|
239
|
+
reactContext
|
|
240
|
+
.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter::class.java)
|
|
241
|
+
.emit("DVAIBridgeProgress", payload)
|
|
242
|
+
} catch (_: Throwable) {
|
|
243
|
+
// The JS context may have been torn down (e.g. fast-refresh
|
|
244
|
+
// in dev). Swallow rather than crash the host app.
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// MARK: - Helpers
|
|
249
|
+
|
|
250
|
+
private fun parseStartOptions(opts: ReadableMap): StartOptions {
|
|
251
|
+
val backendStr = opts.getString("backend")
|
|
252
|
+
?: throw DVAIBridgeError.ConfigurationInvalid("StartOptions: missing `backend`")
|
|
253
|
+
val backend = BackendKind.fromJsName(backendStr)
|
|
254
|
+
?: throw DVAIBridgeError.BackendUnavailable(
|
|
255
|
+
BackendKind.Auto,
|
|
256
|
+
"Backend \"$backendStr\" is not available on Android.",
|
|
257
|
+
)
|
|
258
|
+
|
|
259
|
+
val cors: CorsConfig = opts.dynamicOrNull("corsOrigin")?.let { raw ->
|
|
260
|
+
when (raw) {
|
|
261
|
+
is String -> if (raw == "*") CorsConfig.Wildcard else CorsConfig.Exact(raw)
|
|
262
|
+
is ReadableArray -> CorsConfig.Allowlist(raw.toStringList())
|
|
263
|
+
else -> CorsConfig.Wildcard
|
|
264
|
+
}
|
|
265
|
+
} ?: CorsConfig.Wildcard
|
|
266
|
+
|
|
267
|
+
return StartOptions(
|
|
268
|
+
backend = backend,
|
|
269
|
+
modelPath = opts.getStringOrNull("modelPath"),
|
|
270
|
+
tokenizerPath = opts.getStringOrNull("tokenizerPath"),
|
|
271
|
+
mmprojPath = opts.getStringOrNull("mmprojPath"),
|
|
272
|
+
chatTemplate = opts.getStringOrNull("chatTemplate"),
|
|
273
|
+
modelId = opts.getStringOrNull("modelId"),
|
|
274
|
+
gpuLayers = opts.getIntOrNull("gpuLayers") ?: 99,
|
|
275
|
+
contextSize = opts.getIntOrNull("contextSize") ?: 2048,
|
|
276
|
+
threads = opts.getIntOrNull("threads") ?: 4,
|
|
277
|
+
embeddingMode = opts.getBooleanOrNull("embeddingMode") ?: false,
|
|
278
|
+
visionEnabled = opts.getBooleanOrNull("visionEnabled") ?: false,
|
|
279
|
+
temperature = opts.getDoubleOrNull("temperature")?.toFloat() ?: 0f,
|
|
280
|
+
topP = opts.getDoubleOrNull("topP")?.toFloat() ?: 1f,
|
|
281
|
+
topK = opts.getIntOrNull("topK") ?: 0,
|
|
282
|
+
maxNewTokens = opts.getIntOrNull("maxNewTokens") ?: 512,
|
|
283
|
+
httpBasePort = opts.getIntOrNull("httpBasePort") ?: 38883,
|
|
284
|
+
httpMaxPortAttempts = opts.getIntOrNull("httpMaxPortAttempts") ?: 16,
|
|
285
|
+
corsOrigin = cors,
|
|
286
|
+
)
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// MARK: - Conversions
|
|
291
|
+
|
|
292
|
+
/** Convert a Kotlin `BackendKind` to the lowercase JS-facing name. */
|
|
293
|
+
internal fun BackendKind.toJsName(): String = when (this) {
|
|
294
|
+
BackendKind.Auto -> "auto"
|
|
295
|
+
BackendKind.Llama -> "llama"
|
|
296
|
+
BackendKind.MediaPipe -> "mediapipe"
|
|
297
|
+
BackendKind.LiteRT -> "litert"
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
/** Convert v3.2 PrecheckMode → kebab-case JS-facing string. */
|
|
301
|
+
internal fun co.deepvoiceai.bridge.shared.core.capability.PrecheckMode.toJsName(): String = when (this) {
|
|
302
|
+
co.deepvoiceai.bridge.shared.core.capability.PrecheckMode.OK -> "ok"
|
|
303
|
+
co.deepvoiceai.bridge.shared.core.capability.PrecheckMode.OFFLOAD_ONLY -> "offload-only"
|
|
304
|
+
co.deepvoiceai.bridge.shared.core.capability.PrecheckMode.TOO_WEAK -> "too-weak"
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
/** Convert v3.2 GpuClass → kebab-case JS-facing string. */
|
|
308
|
+
internal fun co.deepvoiceai.bridge.shared.core.capability.GpuClass.toJsName(): String = when (this) {
|
|
309
|
+
co.deepvoiceai.bridge.shared.core.capability.GpuClass.NONE -> "none"
|
|
310
|
+
co.deepvoiceai.bridge.shared.core.capability.GpuClass.INTEGRATED -> "integrated"
|
|
311
|
+
co.deepvoiceai.bridge.shared.core.capability.GpuClass.DISCRETE -> "discrete"
|
|
312
|
+
co.deepvoiceai.bridge.shared.core.capability.GpuClass.APPLE_SILICON -> "apple-silicon"
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
/** Convert v3.2 CpuClass → JS-facing string. */
|
|
316
|
+
internal fun co.deepvoiceai.bridge.shared.core.capability.CpuClass.toJsName(): String = when (this) {
|
|
317
|
+
co.deepvoiceai.bridge.shared.core.capability.CpuClass.LOW -> "low"
|
|
318
|
+
co.deepvoiceai.bridge.shared.core.capability.CpuClass.MID -> "mid"
|
|
319
|
+
co.deepvoiceai.bridge.shared.core.capability.CpuClass.HIGH -> "high"
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
/**
|
|
323
|
+
* Look up the Kotlin `BackendKind` for a JS-side backend string. Returns
|
|
324
|
+
* null for iOS-only kinds (`foundation`, `coreml`, `mlx`) — the caller
|
|
325
|
+
* raises a `BackendUnavailable` error in that case so the JS side gets
|
|
326
|
+
* a stable failure mode.
|
|
327
|
+
*/
|
|
328
|
+
internal fun BackendKind.Companion.fromJsName(name: String): BackendKind? = when (name) {
|
|
329
|
+
"auto" -> BackendKind.Auto
|
|
330
|
+
"llama" -> BackendKind.Llama
|
|
331
|
+
"mediapipe" -> BackendKind.MediaPipe
|
|
332
|
+
"litert" -> BackendKind.LiteRT
|
|
333
|
+
else -> null
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
/**
|
|
337
|
+
* Convert a Kotlin `ProgressEvent` into the JS event-emitter payload shape
|
|
338
|
+
* `{ kind, phase, percent?, message?, error? }`.
|
|
339
|
+
*
|
|
340
|
+
* The Android `ProgressEvent.phase` is a free-form string already (matches
|
|
341
|
+
* the JS-side `"start" | "stop" | "download"` discriminator), so this
|
|
342
|
+
* mostly just re-shapes the field set.
|
|
343
|
+
*/
|
|
344
|
+
internal fun ProgressEvent.toJsEvent(): WritableMap = Arguments.createMap().apply {
|
|
345
|
+
when (val ev = this@toJsEvent) {
|
|
346
|
+
is ProgressEvent.Started -> {
|
|
347
|
+
putString("kind", "started")
|
|
348
|
+
putString("phase", ev.phase)
|
|
349
|
+
}
|
|
350
|
+
is ProgressEvent.Progress -> {
|
|
351
|
+
putString("kind", "progress")
|
|
352
|
+
putString("phase", ev.phase)
|
|
353
|
+
// Android emits percent as 0..1 float (with -1 for indeterminate);
|
|
354
|
+
// JS expects 0..100 with omission for indeterminate.
|
|
355
|
+
if (ev.percent >= 0f) {
|
|
356
|
+
putDouble("percent", (ev.percent * 100.0).coerceIn(0.0, 100.0))
|
|
357
|
+
}
|
|
358
|
+
if (ev.message.isNotEmpty()) putString("message", ev.message)
|
|
359
|
+
}
|
|
360
|
+
is ProgressEvent.Completed -> {
|
|
361
|
+
putString("kind", "completed")
|
|
362
|
+
putString("phase", ev.phase)
|
|
363
|
+
}
|
|
364
|
+
is ProgressEvent.Failed -> {
|
|
365
|
+
putString("kind", "failed")
|
|
366
|
+
putString("phase", ev.phase)
|
|
367
|
+
val errMap = Arguments.createMap().apply {
|
|
368
|
+
putString("kind", ev.error.toErrorKind())
|
|
369
|
+
putString("message", ev.error.message ?: ev.error::class.simpleName.orEmpty())
|
|
370
|
+
}
|
|
371
|
+
putMap("error", errMap)
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
/**
|
|
377
|
+
* Stable JS-side `kind` discriminator for a `DVAIBridgeError` instance.
|
|
378
|
+
* Mirrors `DVAIBridgeErrorKind` in `src/types.ts`.
|
|
379
|
+
*/
|
|
380
|
+
internal fun DVAIBridgeError.toErrorKind(): String = when (this) {
|
|
381
|
+
is DVAIBridgeError.AlreadyStarted -> "alreadyStarted"
|
|
382
|
+
is DVAIBridgeError.ConfigurationInvalid -> "configurationInvalid"
|
|
383
|
+
is DVAIBridgeError.ModelLoadFailed -> "modelLoadFailed"
|
|
384
|
+
is DVAIBridgeError.BackendUnavailable -> "backendUnavailable"
|
|
385
|
+
is DVAIBridgeError.BackendError -> "backendError"
|
|
386
|
+
is DVAIBridgeError.ChecksumMismatch -> "checksumMismatch"
|
|
387
|
+
is DVAIBridgeError.DownloadFailed -> "downloadFailed"
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
/** Reject a Promise with the canonical `DVAIBridgeError` shape. */
|
|
391
|
+
internal fun Promise.rejectWith(err: DVAIBridgeError) {
|
|
392
|
+
val code = err.toErrorKind()
|
|
393
|
+
val message = err.message ?: err::class.simpleName.orEmpty()
|
|
394
|
+
val userInfo = Arguments.createMap().apply {
|
|
395
|
+
putString("kind", code)
|
|
396
|
+
when (err) {
|
|
397
|
+
is DVAIBridgeError.AlreadyStarted -> {
|
|
398
|
+
putString("currentBackend", err.currentBackend.toJsName())
|
|
399
|
+
putString("baseUrl", err.baseUrl)
|
|
400
|
+
}
|
|
401
|
+
is DVAIBridgeError.BackendUnavailable -> {
|
|
402
|
+
putString("backend", err.backend.toJsName())
|
|
403
|
+
}
|
|
404
|
+
else -> Unit
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
this.reject(code, message, err, userInfo)
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
// MARK: - ReadableMap convenience
|
|
411
|
+
|
|
412
|
+
private fun ReadableMap.getStringOrNull(key: String): String? =
|
|
413
|
+
if (hasKey(key) && !isNull(key)) getString(key) else null
|
|
414
|
+
|
|
415
|
+
private fun ReadableMap.getIntOrNull(key: String): Int? =
|
|
416
|
+
if (hasKey(key) && !isNull(key)) getInt(key) else null
|
|
417
|
+
|
|
418
|
+
private fun ReadableMap.getBooleanOrNull(key: String): Boolean? =
|
|
419
|
+
if (hasKey(key) && !isNull(key)) getBoolean(key) else null
|
|
420
|
+
|
|
421
|
+
private fun ReadableMap.getDoubleOrNull(key: String): Double? =
|
|
422
|
+
if (hasKey(key) && !isNull(key)) getDouble(key) else null
|
|
423
|
+
|
|
424
|
+
/** Returns a Kotlin Any? for keys whose dynamic type isn't known up-front. */
|
|
425
|
+
private fun ReadableMap.dynamicOrNull(key: String): Any? {
|
|
426
|
+
if (!hasKey(key) || isNull(key)) return null
|
|
427
|
+
return when (getType(key)) {
|
|
428
|
+
com.facebook.react.bridge.ReadableType.String -> getString(key)
|
|
429
|
+
com.facebook.react.bridge.ReadableType.Array -> getArray(key)
|
|
430
|
+
com.facebook.react.bridge.ReadableType.Map -> getMap(key)
|
|
431
|
+
com.facebook.react.bridge.ReadableType.Boolean -> getBoolean(key)
|
|
432
|
+
com.facebook.react.bridge.ReadableType.Number -> getDouble(key)
|
|
433
|
+
else -> null
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
private fun ReadableArray.toStringList(): List<String> {
|
|
438
|
+
val out = ArrayList<String>(size())
|
|
439
|
+
for (i in 0 until size()) {
|
|
440
|
+
out += getString(i)
|
|
441
|
+
}
|
|
442
|
+
return out
|
|
443
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
package co.deepvoiceai.bridge.rn
|
|
2
|
+
|
|
3
|
+
import com.facebook.react.TurboReactPackage
|
|
4
|
+
import com.facebook.react.bridge.NativeModule
|
|
5
|
+
import com.facebook.react.bridge.ReactApplicationContext
|
|
6
|
+
import com.facebook.react.module.model.ReactModuleInfo
|
|
7
|
+
import com.facebook.react.module.model.ReactModuleInfoProvider
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* `ReactPackage` registration for the DVAIBridge TurboModule. Picked up by
|
|
11
|
+
* RN's autolinking via `react-native.config.js` — the consumer's
|
|
12
|
+
* `MainApplication` doesn't need to add anything manually.
|
|
13
|
+
*
|
|
14
|
+
* `TurboReactPackage` (rather than the legacy `ReactPackage`) lets the
|
|
15
|
+
* package satisfy both old-arch and new-arch (TurboModule) registration
|
|
16
|
+
* paths from a single class.
|
|
17
|
+
*/
|
|
18
|
+
class DVAIBridgePackage : TurboReactPackage() {
|
|
19
|
+
|
|
20
|
+
override fun getModule(name: String, reactContext: ReactApplicationContext): NativeModule? {
|
|
21
|
+
return if (name == DVAIBridgeNativeModule.NAME) {
|
|
22
|
+
DVAIBridgeNativeModule(reactContext)
|
|
23
|
+
} else {
|
|
24
|
+
null
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
override fun getReactModuleInfoProvider(): ReactModuleInfoProvider {
|
|
29
|
+
return ReactModuleInfoProvider {
|
|
30
|
+
mapOf(
|
|
31
|
+
DVAIBridgeNativeModule.NAME to ReactModuleInfo(
|
|
32
|
+
DVAIBridgeNativeModule.NAME,
|
|
33
|
+
DVAIBridgeNativeModule::class.java.name,
|
|
34
|
+
/* canOverrideExistingModule = */ false,
|
|
35
|
+
/* needsEagerInit = */ false,
|
|
36
|
+
/* isCxxModule = */ false,
|
|
37
|
+
/* isTurboModule = */ BuildConfig.IS_NEW_ARCHITECTURE_ENABLED,
|
|
38
|
+
),
|
|
39
|
+
)
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
package co.deepvoiceai.bridge.rn
|
|
2
|
+
|
|
3
|
+
import com.facebook.react.bridge.Promise
|
|
4
|
+
import com.facebook.react.bridge.ReactApplicationContext
|
|
5
|
+
import com.facebook.react.bridge.ReadableMap
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* New Architecture (TurboModule) variant. Subclasses the codegen-generated
|
|
9
|
+
* `NativeDVAIBridgeSpec` (emitted at Gradle sync from
|
|
10
|
+
* `src/NativeDVAIBridge.ts` via the `codegenConfig` block in `package.json`).
|
|
11
|
+
*
|
|
12
|
+
* All actual logic lives in [DVAIBridgeNativeModuleImpl] — this class is a
|
|
13
|
+
* thin forwarder so the impl is shared with the old-arch variant under
|
|
14
|
+
* `src/oldarch/`.
|
|
15
|
+
*/
|
|
16
|
+
class DVAIBridgeNativeModule(
|
|
17
|
+
reactContext: ReactApplicationContext,
|
|
18
|
+
) : NativeDVAIBridgeSpec(reactContext) {
|
|
19
|
+
|
|
20
|
+
private val impl = DVAIBridgeNativeModuleImpl(reactContext)
|
|
21
|
+
|
|
22
|
+
override fun getName(): String = NAME
|
|
23
|
+
|
|
24
|
+
override fun startBridge(opts: ReadableMap, promise: Promise) {
|
|
25
|
+
impl.startBridge(opts, promise)
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
override fun stopBridge(promise: Promise) {
|
|
29
|
+
impl.stopBridge(promise)
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
override fun status(promise: Promise) {
|
|
33
|
+
impl.status(promise)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
override fun downloadModel(opts: ReadableMap, promise: Promise) {
|
|
37
|
+
impl.downloadModel(opts, promise)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
override fun assessHardware(
|
|
41
|
+
hardwareMinimum: Double,
|
|
42
|
+
minLocalCapability: Double,
|
|
43
|
+
promise: Promise,
|
|
44
|
+
) {
|
|
45
|
+
impl.assessHardware(hardwareMinimum, minLocalCapability, promise)
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
override fun addListener(eventName: String) {
|
|
49
|
+
impl.addListener(eventName)
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
override fun removeListeners(count: Double) {
|
|
53
|
+
impl.removeListeners(count.toInt())
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
override fun invalidate() {
|
|
57
|
+
impl.invalidate()
|
|
58
|
+
super.invalidate()
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
companion object {
|
|
62
|
+
const val NAME = "DVAIBridge"
|
|
63
|
+
}
|
|
64
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
package co.deepvoiceai.bridge.rn
|
|
2
|
+
|
|
3
|
+
import com.facebook.react.bridge.Promise
|
|
4
|
+
import com.facebook.react.bridge.ReactApplicationContext
|
|
5
|
+
import com.facebook.react.bridge.ReactContextBaseJavaModule
|
|
6
|
+
import com.facebook.react.bridge.ReactMethod
|
|
7
|
+
import com.facebook.react.bridge.ReadableMap
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Old Architecture (legacy bridge) variant of the DVAIBridge React Native
|
|
11
|
+
* module. Used when `newArchEnabled=false` (Bridgeless OFF). Picked up by
|
|
12
|
+
* the source-set switch in `android/build.gradle`.
|
|
13
|
+
*
|
|
14
|
+
* RN ≥ 0.77 ships with Bridgeless ON by default, so this class exists
|
|
15
|
+
* mainly as a safety net for downstream consumers temporarily forced onto
|
|
16
|
+
* the legacy bridge while migrating. All real logic lives in
|
|
17
|
+
* [DVAIBridgeNativeModuleImpl].
|
|
18
|
+
*/
|
|
19
|
+
class DVAIBridgeNativeModule(
|
|
20
|
+
reactContext: ReactApplicationContext,
|
|
21
|
+
) : ReactContextBaseJavaModule(reactContext) {
|
|
22
|
+
|
|
23
|
+
private val impl = DVAIBridgeNativeModuleImpl(reactContext)
|
|
24
|
+
|
|
25
|
+
override fun getName(): String = NAME
|
|
26
|
+
|
|
27
|
+
@ReactMethod
|
|
28
|
+
fun startBridge(opts: ReadableMap, promise: Promise) {
|
|
29
|
+
impl.startBridge(opts, promise)
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
@ReactMethod
|
|
33
|
+
fun stopBridge(promise: Promise) {
|
|
34
|
+
impl.stopBridge(promise)
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
@ReactMethod
|
|
38
|
+
fun status(promise: Promise) {
|
|
39
|
+
impl.status(promise)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
@ReactMethod
|
|
43
|
+
fun downloadModel(opts: ReadableMap, promise: Promise) {
|
|
44
|
+
impl.downloadModel(opts, promise)
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
@ReactMethod
|
|
48
|
+
fun assessHardware(hardwareMinimum: Double, minLocalCapability: Double, promise: Promise) {
|
|
49
|
+
impl.assessHardware(hardwareMinimum, minLocalCapability, promise)
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
@ReactMethod
|
|
53
|
+
fun addListener(eventName: String) {
|
|
54
|
+
impl.addListener(eventName)
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
@ReactMethod
|
|
58
|
+
fun removeListeners(count: Double) {
|
|
59
|
+
impl.removeListeners(count.toInt())
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
override fun invalidate() {
|
|
63
|
+
impl.invalidate()
|
|
64
|
+
super.invalidate()
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
companion object {
|
|
68
|
+
const val NAME = "DVAIBridge"
|
|
69
|
+
}
|
|
70
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@dvai-bridge/react-native",
|
|
3
|
+
"version": "4.0.0",
|
|
4
|
+
"publishConfig": {
|
|
5
|
+
"registry": "https://registry.npmjs.org/",
|
|
6
|
+
"access": "public"
|
|
7
|
+
},
|
|
8
|
+
"description": "React Native TurboModule wrapping the @dvai-bridge/ios + @dvai-bridge/android native SDKs behind a shared TS API.",
|
|
9
|
+
"main": "lib/commonjs/index",
|
|
10
|
+
"module": "lib/module/index",
|
|
11
|
+
"types": "lib/typescript/commonjs/src/index.d.ts",
|
|
12
|
+
"react-native": "src/index",
|
|
13
|
+
"source": "src/index",
|
|
14
|
+
"exports": {
|
|
15
|
+
".": {
|
|
16
|
+
"import": {
|
|
17
|
+
"types": "./lib/typescript/module/src/index.d.ts",
|
|
18
|
+
"default": "./lib/module/index.js"
|
|
19
|
+
},
|
|
20
|
+
"require": {
|
|
21
|
+
"types": "./lib/typescript/commonjs/src/index.d.ts",
|
|
22
|
+
"default": "./lib/commonjs/index.js"
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
},
|
|
26
|
+
"files": [
|
|
27
|
+
"android/src",
|
|
28
|
+
"android/build.gradle",
|
|
29
|
+
"android/gradle.properties",
|
|
30
|
+
"android/settings.gradle",
|
|
31
|
+
"README.md",
|
|
32
|
+
"LICENSE"
|
|
33
|
+
],
|
|
34
|
+
"keywords": [
|
|
35
|
+
"dvai-bridge",
|
|
36
|
+
"react-native",
|
|
37
|
+
"turbomodule",
|
|
38
|
+
"llm",
|
|
39
|
+
"on-device",
|
|
40
|
+
"openai-compatible",
|
|
41
|
+
"llama-cpp",
|
|
42
|
+
"mediapipe",
|
|
43
|
+
"litert",
|
|
44
|
+
"mlx",
|
|
45
|
+
"coreml",
|
|
46
|
+
"foundation-models"
|
|
47
|
+
],
|
|
48
|
+
"author": "Deep Chakraborty <https://github.com/dk013>",
|
|
49
|
+
"license": "Custom (See LICENSE)",
|
|
50
|
+
"repository": {
|
|
51
|
+
"type": "git",
|
|
52
|
+
"url": "https://github.com/westenets/dvai-bridge.git"
|
|
53
|
+
},
|
|
54
|
+
"peerDependencies": {
|
|
55
|
+
"react": ">=18.0.0",
|
|
56
|
+
"react-native": ">=0.77.0"
|
|
57
|
+
},
|
|
58
|
+
"peerDependenciesMeta": {
|
|
59
|
+
"react": {
|
|
60
|
+
"optional": false
|
|
61
|
+
},
|
|
62
|
+
"react-native": {
|
|
63
|
+
"optional": false
|
|
64
|
+
}
|
|
65
|
+
},
|
|
66
|
+
"devDependencies": {
|
|
67
|
+
"@babel/core": "^7.29.0",
|
|
68
|
+
"@babel/preset-env": "^7.29.2",
|
|
69
|
+
"@babel/preset-react": "^7.28.5",
|
|
70
|
+
"@babel/preset-typescript": "^7.28.5",
|
|
71
|
+
"@react-native/babel-preset": "^0.85.2",
|
|
72
|
+
"@react-native/eslint-config": "^0.85.2",
|
|
73
|
+
"@testing-library/react-native": "^13.3.3",
|
|
74
|
+
"@types/jest": "^30.0.0",
|
|
75
|
+
"@types/react": "^19.2.14",
|
|
76
|
+
"babel-jest": "^30.3.0",
|
|
77
|
+
"del-cli": "^7.0.0",
|
|
78
|
+
"jest": "^30.3.0",
|
|
79
|
+
"jest-environment-jsdom": "^30.3.0",
|
|
80
|
+
"react": "^19.2.5",
|
|
81
|
+
"react-native": "^0.85.2",
|
|
82
|
+
"react-native-builder-bob": "^0.41.0",
|
|
83
|
+
"react-test-renderer": "^19.2.5",
|
|
84
|
+
"typescript": "^6.0.3"
|
|
85
|
+
},
|
|
86
|
+
"react-native-builder-bob": {
|
|
87
|
+
"source": "src",
|
|
88
|
+
"output": "lib",
|
|
89
|
+
"targets": [
|
|
90
|
+
[
|
|
91
|
+
"commonjs",
|
|
92
|
+
{
|
|
93
|
+
"esm": true
|
|
94
|
+
}
|
|
95
|
+
],
|
|
96
|
+
[
|
|
97
|
+
"module",
|
|
98
|
+
{
|
|
99
|
+
"esm": true
|
|
100
|
+
}
|
|
101
|
+
],
|
|
102
|
+
[
|
|
103
|
+
"typescript",
|
|
104
|
+
{
|
|
105
|
+
"project": "tsconfig.build.json"
|
|
106
|
+
}
|
|
107
|
+
]
|
|
108
|
+
]
|
|
109
|
+
},
|
|
110
|
+
"codegenConfig": {
|
|
111
|
+
"name": "RNDVAIBridgeSpec",
|
|
112
|
+
"type": "modules",
|
|
113
|
+
"jsSrcsDir": "src",
|
|
114
|
+
"android": {
|
|
115
|
+
"javaPackageName": "co.deepvoiceai.bridge.rn"
|
|
116
|
+
}
|
|
117
|
+
},
|
|
118
|
+
"engines": {
|
|
119
|
+
"node": ">=22"
|
|
120
|
+
},
|
|
121
|
+
"scripts": {
|
|
122
|
+
"build": "bob build",
|
|
123
|
+
"clean": "del-cli lib",
|
|
124
|
+
"test": "jest",
|
|
125
|
+
"typecheck": "tsc --noEmit"
|
|
126
|
+
}
|
|
127
|
+
}
|