cactus-react-native 1.0.2 → 1.1.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/README.md +378 -21
- package/android/src/main/java/com/margelo/nitro/cactus/HybridCactusCrypto.kt +23 -15
- package/android/src/main/java/com/margelo/nitro/cactus/HybridCactusDeviceInfo.kt +12 -9
- package/android/src/main/java/com/margelo/nitro/cactus/HybridCactusFileSystem.kt +42 -41
- package/android/src/main/java/com/margelo/nitro/cactus/HybridCactusImage.kt +81 -0
- package/android/src/main/jniLibs/arm64-v8a/libcactus.a +0 -0
- package/cpp/HybridCactus.cpp +105 -0
- package/cpp/HybridCactus.hpp +13 -0
- package/cpp/cactus_ffi.h +27 -0
- package/ios/HybridCactusImage.swift +53 -0
- package/ios/cactus.xcframework/ios-arm64/cactus.framework/Headers/cactus_ffi.h +27 -0
- package/ios/cactus.xcframework/ios-arm64/cactus.framework/Headers/engine.h +35 -3
- package/ios/cactus.xcframework/ios-arm64/cactus.framework/Headers/ffi_utils.h +10 -9
- package/ios/cactus.xcframework/ios-arm64/cactus.framework/Headers/graph.h +3 -1
- package/ios/cactus.xcframework/ios-arm64/cactus.framework/Headers/kernel.h +31 -0
- package/ios/cactus.xcframework/ios-arm64/cactus.framework/cactus +0 -0
- package/ios/cactus.xcframework/ios-arm64-simulator/cactus.framework/Headers/cactus_ffi.h +27 -0
- package/ios/cactus.xcframework/ios-arm64-simulator/cactus.framework/Headers/engine.h +35 -3
- package/ios/cactus.xcframework/ios-arm64-simulator/cactus.framework/Headers/ffi_utils.h +10 -9
- package/ios/cactus.xcframework/ios-arm64-simulator/cactus.framework/Headers/graph.h +3 -1
- package/ios/cactus.xcframework/ios-arm64-simulator/cactus.framework/Headers/kernel.h +31 -0
- package/ios/cactus.xcframework/ios-arm64-simulator/cactus.framework/cactus +0 -0
- package/lib/module/api/Database.js +23 -0
- package/lib/module/api/Database.js.map +1 -1
- package/lib/module/api/RemoteLM.js +201 -0
- package/lib/module/api/RemoteLM.js.map +1 -0
- package/lib/module/classes/CactusLM.js +52 -26
- package/lib/module/classes/CactusLM.js.map +1 -1
- package/lib/module/classes/CactusSTT.js +137 -0
- package/lib/module/classes/CactusSTT.js.map +1 -0
- package/lib/module/config/CactusConfig.js +4 -0
- package/lib/module/config/CactusConfig.js.map +1 -1
- package/lib/module/constants/packageVersion.js +1 -1
- package/lib/module/hooks/useCactusLM.js +33 -10
- package/lib/module/hooks/useCactusLM.js.map +1 -1
- package/lib/module/hooks/useCactusSTT.js +234 -0
- package/lib/module/hooks/useCactusSTT.js.map +1 -0
- package/lib/module/index.js +2 -0
- package/lib/module/index.js.map +1 -1
- package/lib/module/native/Cactus.js +50 -1
- package/lib/module/native/Cactus.js.map +1 -1
- package/lib/module/native/CactusFileSystem.js +2 -3
- package/lib/module/native/CactusFileSystem.js.map +1 -1
- package/lib/module/native/CactusImage.js +13 -0
- package/lib/module/native/CactusImage.js.map +1 -0
- package/lib/module/native/index.js +1 -0
- package/lib/module/native/index.js.map +1 -1
- package/lib/module/specs/CactusImage.nitro.js +4 -0
- package/lib/module/specs/CactusImage.nitro.js.map +1 -0
- package/lib/module/telemetry/Telemetry.js +53 -1
- package/lib/module/telemetry/Telemetry.js.map +1 -1
- package/lib/module/types/CactusSTT.js +2 -0
- package/lib/module/types/CactusSTT.js.map +1 -0
- package/lib/typescript/src/api/Database.d.ts +1 -0
- package/lib/typescript/src/api/Database.d.ts.map +1 -1
- package/lib/typescript/src/api/RemoteLM.d.ts +14 -0
- package/lib/typescript/src/api/RemoteLM.d.ts.map +1 -0
- package/lib/typescript/src/classes/CactusLM.d.ts +6 -4
- package/lib/typescript/src/classes/CactusLM.d.ts.map +1 -1
- package/lib/typescript/src/classes/CactusSTT.d.ts +25 -0
- package/lib/typescript/src/classes/CactusSTT.d.ts.map +1 -0
- package/lib/typescript/src/config/CactusConfig.d.ts +1 -0
- package/lib/typescript/src/config/CactusConfig.d.ts.map +1 -1
- package/lib/typescript/src/constants/packageVersion.d.ts +1 -1
- package/lib/typescript/src/hooks/useCactusLM.d.ts +4 -3
- package/lib/typescript/src/hooks/useCactusLM.d.ts.map +1 -1
- package/lib/typescript/src/hooks/useCactusSTT.d.ts +20 -0
- package/lib/typescript/src/hooks/useCactusSTT.d.ts.map +1 -0
- package/lib/typescript/src/index.d.ts +4 -1
- package/lib/typescript/src/index.d.ts.map +1 -1
- package/lib/typescript/src/native/Cactus.d.ts +9 -2
- package/lib/typescript/src/native/Cactus.d.ts.map +1 -1
- package/lib/typescript/src/native/CactusFileSystem.d.ts +1 -1
- package/lib/typescript/src/native/CactusFileSystem.d.ts.map +1 -1
- package/lib/typescript/src/native/CactusImage.d.ts +6 -0
- package/lib/typescript/src/native/CactusImage.d.ts.map +1 -0
- package/lib/typescript/src/native/index.d.ts +1 -0
- package/lib/typescript/src/native/index.d.ts.map +1 -1
- package/lib/typescript/src/specs/Cactus.nitro.d.ts +3 -0
- package/lib/typescript/src/specs/Cactus.nitro.d.ts.map +1 -1
- package/lib/typescript/src/specs/CactusImage.nitro.d.ts +9 -0
- package/lib/typescript/src/specs/CactusImage.nitro.d.ts.map +1 -0
- package/lib/typescript/src/telemetry/Telemetry.d.ts +5 -1
- package/lib/typescript/src/telemetry/Telemetry.d.ts.map +1 -1
- package/lib/typescript/src/types/CactusLM.d.ts +8 -5
- package/lib/typescript/src/types/CactusLM.d.ts.map +1 -1
- package/lib/typescript/src/types/CactusSTT.d.ts +37 -0
- package/lib/typescript/src/types/CactusSTT.d.ts.map +1 -0
- package/nitro.json +4 -0
- package/nitrogen/generated/android/c++/JHybridCactusImageSpec.cpp +81 -0
- package/nitrogen/generated/android/c++/JHybridCactusImageSpec.hpp +66 -0
- package/nitrogen/generated/android/cactus+autolinking.cmake +2 -0
- package/nitrogen/generated/android/cactusOnLoad.cpp +10 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/cactus/HybridCactusImageSpec.kt +62 -0
- package/nitrogen/generated/ios/Cactus-Swift-Cxx-Bridge.cpp +17 -0
- package/nitrogen/generated/ios/Cactus-Swift-Cxx-Bridge.hpp +17 -0
- package/nitrogen/generated/ios/Cactus-Swift-Cxx-Umbrella.hpp +5 -0
- package/nitrogen/generated/ios/CactusAutolinking.mm +8 -0
- package/nitrogen/generated/ios/CactusAutolinking.swift +15 -0
- package/nitrogen/generated/ios/c++/HybridCactusImageSpecSwift.cpp +11 -0
- package/nitrogen/generated/ios/c++/HybridCactusImageSpecSwift.hpp +85 -0
- package/nitrogen/generated/ios/swift/HybridCactusImageSpec.swift +58 -0
- package/nitrogen/generated/ios/swift/HybridCactusImageSpec_cxx.swift +158 -0
- package/nitrogen/generated/shared/c++/HybridCactusImageSpec.cpp +22 -0
- package/nitrogen/generated/shared/c++/HybridCactusImageSpec.hpp +64 -0
- package/nitrogen/generated/shared/c++/HybridCactusSpec.cpp +3 -0
- package/nitrogen/generated/shared/c++/HybridCactusSpec.hpp +3 -0
- package/package.json +1 -1
- package/src/api/Database.ts +27 -0
- package/src/api/RemoteLM.ts +273 -0
- package/src/classes/CactusLM.ts +72 -38
- package/src/classes/CactusSTT.ts +182 -0
- package/src/config/CactusConfig.ts +4 -0
- package/src/constants/packageVersion.ts +1 -1
- package/src/hooks/useCactusLM.ts +45 -17
- package/src/hooks/useCactusSTT.ts +285 -0
- package/src/index.tsx +14 -2
- package/src/native/Cactus.ts +94 -4
- package/src/native/CactusFileSystem.ts +2 -2
- package/src/native/CactusImage.ts +20 -0
- package/src/native/index.ts +1 -0
- package/src/specs/Cactus.nitro.ts +9 -0
- package/src/specs/CactusImage.nitro.ts +12 -0
- package/src/telemetry/Telemetry.ts +78 -1
- package/src/types/CactusLM.ts +9 -5
- package/src/types/CactusSTT.ts +42 -0
|
@@ -16,29 +16,28 @@ import kotlin.math.floor
|
|
|
16
16
|
class HybridCactusFileSystem : HybridCactusFileSystemSpec() {
|
|
17
17
|
private val context = NitroModules.applicationContext ?: error("Android context not found")
|
|
18
18
|
|
|
19
|
-
override fun getCactusDirectory(): Promise<String> {
|
|
20
|
-
return Promise.async { cactusFile().absolutePath }
|
|
21
|
-
}
|
|
19
|
+
override fun getCactusDirectory(): Promise<String> = Promise.async { cactusFile().absolutePath }
|
|
22
20
|
|
|
23
|
-
override fun fileExists(path: String): Promise<Boolean>
|
|
24
|
-
|
|
21
|
+
override fun fileExists(path: String): Promise<Boolean> =
|
|
22
|
+
Promise.async {
|
|
25
23
|
val cactusDir = cactusFile()
|
|
26
24
|
val file = File(cactusDir, path)
|
|
27
25
|
file.exists()
|
|
28
26
|
}
|
|
29
|
-
}
|
|
30
27
|
|
|
31
|
-
override fun writeFile(
|
|
32
|
-
|
|
28
|
+
override fun writeFile(
|
|
29
|
+
path: String,
|
|
30
|
+
content: String,
|
|
31
|
+
): Promise<Unit> =
|
|
32
|
+
Promise.async {
|
|
33
33
|
val cactusDir = cactusFile()
|
|
34
34
|
val file = File(cactusDir, path)
|
|
35
35
|
file.parentFile?.mkdirs()
|
|
36
36
|
file.writeText(content)
|
|
37
37
|
}
|
|
38
|
-
}
|
|
39
38
|
|
|
40
|
-
override fun readFile(path: String): Promise<String>
|
|
41
|
-
|
|
39
|
+
override fun readFile(path: String): Promise<String> =
|
|
40
|
+
Promise.async {
|
|
42
41
|
val cactusDir = cactusFile()
|
|
43
42
|
val file = File(cactusDir, path)
|
|
44
43
|
|
|
@@ -48,10 +47,9 @@ class HybridCactusFileSystem : HybridCactusFileSystemSpec() {
|
|
|
48
47
|
|
|
49
48
|
file.readText()
|
|
50
49
|
}
|
|
51
|
-
}
|
|
52
50
|
|
|
53
|
-
override fun deleteFile(path: String): Promise<Unit>
|
|
54
|
-
|
|
51
|
+
override fun deleteFile(path: String): Promise<Unit> =
|
|
52
|
+
Promise.async {
|
|
55
53
|
val cactusDir = cactusFile()
|
|
56
54
|
val file = File(cactusDir, path)
|
|
57
55
|
|
|
@@ -61,18 +59,15 @@ class HybridCactusFileSystem : HybridCactusFileSystemSpec() {
|
|
|
61
59
|
|
|
62
60
|
file.deleteRecursively()
|
|
63
61
|
}
|
|
64
|
-
}
|
|
65
62
|
|
|
66
|
-
override fun modelExists(model: String): Promise<Boolean> {
|
|
67
|
-
return Promise.async { modelFile(model).exists() }
|
|
68
|
-
}
|
|
63
|
+
override fun modelExists(model: String): Promise<Boolean> = Promise.async { modelFile(model).exists() }
|
|
69
64
|
|
|
70
|
-
override fun getModelPath(model: String): Promise<String> {
|
|
71
|
-
return Promise.async { modelFile(model).absolutePath }
|
|
72
|
-
}
|
|
65
|
+
override fun getModelPath(model: String): Promise<String> = Promise.async { modelFile(model).absolutePath }
|
|
73
66
|
|
|
74
67
|
override fun downloadModel(
|
|
75
|
-
model: String,
|
|
68
|
+
model: String,
|
|
69
|
+
from: String,
|
|
70
|
+
callback: ((progress: Double) -> Unit)?,
|
|
76
71
|
): Promise<Unit> {
|
|
77
72
|
return Promise.async {
|
|
78
73
|
val modelFile = modelFile(model)
|
|
@@ -82,21 +77,23 @@ class HybridCactusFileSystem : HybridCactusFileSystemSpec() {
|
|
|
82
77
|
return@async
|
|
83
78
|
}
|
|
84
79
|
|
|
85
|
-
val url =
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
80
|
+
val url =
|
|
81
|
+
try {
|
|
82
|
+
URL(from)
|
|
83
|
+
} catch (_: Throwable) {
|
|
84
|
+
throw Error("Invalid URL")
|
|
85
|
+
}
|
|
90
86
|
|
|
91
87
|
val tmpZip = File.createTempFile("dl_", ".zip", context.cacheDir)
|
|
92
88
|
var connection: HttpURLConnection? = null
|
|
93
89
|
|
|
94
90
|
try {
|
|
95
|
-
connection =
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
91
|
+
connection =
|
|
92
|
+
(url.openConnection() as HttpURLConnection).apply {
|
|
93
|
+
connectTimeout = 30_000
|
|
94
|
+
readTimeout = 5 * 60_000
|
|
95
|
+
instanceFollowRedirects = true
|
|
96
|
+
}
|
|
100
97
|
connection.connect()
|
|
101
98
|
val code = connection.responseCode
|
|
102
99
|
|
|
@@ -129,11 +126,13 @@ class HybridCactusFileSystem : HybridCactusFileSystemSpec() {
|
|
|
129
126
|
|
|
130
127
|
if (contentLength > 0) {
|
|
131
128
|
// cap at 0.99; 1.0 will be sent after unzip
|
|
132
|
-
val pct =
|
|
133
|
-
(
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
129
|
+
val pct =
|
|
130
|
+
floor(
|
|
131
|
+
(downloaded.toDouble() / contentLength.toDouble()).coerceIn(
|
|
132
|
+
0.0,
|
|
133
|
+
1.0,
|
|
134
|
+
) * 99,
|
|
135
|
+
) / 100.0
|
|
137
136
|
|
|
138
137
|
if (pct - lastPct >= 0.01) {
|
|
139
138
|
callback?.invoke(pct)
|
|
@@ -160,7 +159,10 @@ class HybridCactusFileSystem : HybridCactusFileSystemSpec() {
|
|
|
160
159
|
}
|
|
161
160
|
}
|
|
162
161
|
|
|
163
|
-
private fun unzipItem(
|
|
162
|
+
private fun unzipItem(
|
|
163
|
+
zipFile: File,
|
|
164
|
+
outDir: File,
|
|
165
|
+
) {
|
|
164
166
|
val outRoot = outDir.canonicalFile
|
|
165
167
|
ZipInputStream(BufferedInputStream(FileInputStream(zipFile))).use { zis ->
|
|
166
168
|
var entry: ZipEntry? = zis.nextEntry
|
|
@@ -196,8 +198,8 @@ class HybridCactusFileSystem : HybridCactusFileSystemSpec() {
|
|
|
196
198
|
}
|
|
197
199
|
}
|
|
198
200
|
|
|
199
|
-
override fun deleteModel(model: String): Promise<Unit>
|
|
200
|
-
|
|
201
|
+
override fun deleteModel(model: String): Promise<Unit> =
|
|
202
|
+
Promise.async {
|
|
201
203
|
val modelFile = modelFile(model)
|
|
202
204
|
|
|
203
205
|
if (!modelFile.exists()) {
|
|
@@ -206,7 +208,6 @@ class HybridCactusFileSystem : HybridCactusFileSystemSpec() {
|
|
|
206
208
|
|
|
207
209
|
modelFile.deleteRecursively()
|
|
208
210
|
}
|
|
209
|
-
}
|
|
210
211
|
|
|
211
212
|
private fun cactusFile(): File {
|
|
212
213
|
val documentsDir =
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
package com.margelo.nitro.cactus
|
|
2
|
+
|
|
3
|
+
import android.graphics.Bitmap
|
|
4
|
+
import android.graphics.BitmapFactory
|
|
5
|
+
import androidx.core.graphics.createBitmap
|
|
6
|
+
import com.margelo.nitro.NitroModules
|
|
7
|
+
import com.margelo.nitro.core.Promise
|
|
8
|
+
import java.io.File
|
|
9
|
+
import java.io.FileOutputStream
|
|
10
|
+
import java.util.UUID
|
|
11
|
+
|
|
12
|
+
class HybridCactusImage : HybridCactusImageSpec() {
|
|
13
|
+
private val context = NitroModules.applicationContext ?: error("Android context not found")
|
|
14
|
+
|
|
15
|
+
override fun base64(path: String): Promise<String> =
|
|
16
|
+
Promise.async {
|
|
17
|
+
val file = File(path)
|
|
18
|
+
|
|
19
|
+
if (!file.exists()) {
|
|
20
|
+
throw Error("No such file: $path")
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
val imageData = file.readBytes()
|
|
24
|
+
android.util.Base64.encodeToString(imageData, android.util.Base64.NO_WRAP)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
override fun resize(
|
|
28
|
+
path: String,
|
|
29
|
+
height: Double,
|
|
30
|
+
width: Double,
|
|
31
|
+
quality: Double,
|
|
32
|
+
): Promise<String> =
|
|
33
|
+
Promise.async {
|
|
34
|
+
val file = File(path)
|
|
35
|
+
|
|
36
|
+
if (!file.exists()) {
|
|
37
|
+
throw Error("No such file: $path")
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
val bitmap = BitmapFactory.decodeFile(path)
|
|
41
|
+
if (bitmap == null) {
|
|
42
|
+
throw Error("Failed to load image from: $path")
|
|
43
|
+
}
|
|
44
|
+
val resizedBitmap = createBitmap(width.toInt(), height.toInt(), Bitmap.Config.RGB_565)
|
|
45
|
+
val canvas = android.graphics.Canvas(resizedBitmap)
|
|
46
|
+
val paint =
|
|
47
|
+
android.graphics.Paint().apply {
|
|
48
|
+
isFilterBitmap = true
|
|
49
|
+
isAntiAlias = true
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
val scaleX = width.toFloat() / bitmap.width
|
|
53
|
+
val scaleY = height.toFloat() / bitmap.height
|
|
54
|
+
val matrix =
|
|
55
|
+
android.graphics.Matrix().apply {
|
|
56
|
+
setScale(scaleX, scaleY)
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
canvas.drawBitmap(bitmap, matrix, paint)
|
|
60
|
+
bitmap.recycle()
|
|
61
|
+
|
|
62
|
+
val cacheDir = context.cacheDir
|
|
63
|
+
val outputDir = File(cacheDir, "cactus/images")
|
|
64
|
+
outputDir.mkdirs()
|
|
65
|
+
|
|
66
|
+
val fileName = "${UUID.randomUUID()}.jpg"
|
|
67
|
+
val outputFile = File(outputDir, fileName)
|
|
68
|
+
|
|
69
|
+
try {
|
|
70
|
+
FileOutputStream(outputFile).use { outputStream ->
|
|
71
|
+
val qualityInt = (quality * 100).toInt().coerceIn(0, 100)
|
|
72
|
+
resizedBitmap.compress(Bitmap.CompressFormat.JPEG, qualityInt, outputStream)
|
|
73
|
+
}
|
|
74
|
+
resizedBitmap.recycle()
|
|
75
|
+
outputFile.absolutePath
|
|
76
|
+
} catch (e: Exception) {
|
|
77
|
+
resizedBitmap.recycle()
|
|
78
|
+
throw Error("Failed to save resized image: ${e.message}")
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
Binary file
|
package/cpp/HybridCactus.cpp
CHANGED
|
@@ -75,6 +75,53 @@ std::shared_ptr<Promise<std::string>> HybridCactus::complete(
|
|
|
75
75
|
});
|
|
76
76
|
}
|
|
77
77
|
|
|
78
|
+
std::shared_ptr<Promise<std::string>> HybridCactus::transcribe(
|
|
79
|
+
const std::string &audioFilePath, const std::string &prompt,
|
|
80
|
+
double responseBufferSize, const std::optional<std::string> &optionsJson,
|
|
81
|
+
const std::optional<std::function<void(const std::string & /* token */,
|
|
82
|
+
double /* tokenId */)>> &callback) {
|
|
83
|
+
return Promise<std::string>::async([this, audioFilePath, prompt, optionsJson,
|
|
84
|
+
callback,
|
|
85
|
+
responseBufferSize]() -> std::string {
|
|
86
|
+
std::lock_guard<std::mutex> lock(this->_modelMutex);
|
|
87
|
+
|
|
88
|
+
if (!this->_model) {
|
|
89
|
+
throw std::runtime_error("Cactus model is not initialized");
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
struct CallbackCtx {
|
|
93
|
+
const std::function<void(const std::string & /* token */,
|
|
94
|
+
double /* tokenId */)> *callback;
|
|
95
|
+
} callbackCtx{callback.has_value() ? &callback.value() : nullptr};
|
|
96
|
+
|
|
97
|
+
auto cactusTokenCallback = [](const char *token, uint32_t tokenId,
|
|
98
|
+
void *userData) {
|
|
99
|
+
auto *callbackCtx = static_cast<CallbackCtx *>(userData);
|
|
100
|
+
if (!callbackCtx || !callbackCtx->callback || !(*callbackCtx->callback))
|
|
101
|
+
return;
|
|
102
|
+
(*callbackCtx->callback)(token, tokenId);
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
std::string responseBuffer;
|
|
106
|
+
responseBuffer.resize(responseBufferSize);
|
|
107
|
+
|
|
108
|
+
int result =
|
|
109
|
+
cactus_transcribe(this->_model, audioFilePath.c_str(), prompt.c_str(),
|
|
110
|
+
responseBuffer.data(), responseBufferSize,
|
|
111
|
+
optionsJson ? optionsJson->c_str() : nullptr,
|
|
112
|
+
cactusTokenCallback, &callbackCtx);
|
|
113
|
+
|
|
114
|
+
if (result < 0) {
|
|
115
|
+
throw std::runtime_error("Cactus transcription failed");
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Remove null terminator
|
|
119
|
+
responseBuffer.resize(strlen(responseBuffer.c_str()));
|
|
120
|
+
|
|
121
|
+
return responseBuffer;
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
|
|
78
125
|
std::shared_ptr<Promise<std::vector<double>>>
|
|
79
126
|
HybridCactus::embed(const std::string &text, double embeddingBufferSize) {
|
|
80
127
|
return Promise<std::vector<double>>::async(
|
|
@@ -103,6 +150,64 @@ HybridCactus::embed(const std::string &text, double embeddingBufferSize) {
|
|
|
103
150
|
});
|
|
104
151
|
}
|
|
105
152
|
|
|
153
|
+
std::shared_ptr<Promise<std::vector<double>>>
|
|
154
|
+
HybridCactus::imageEmbed(const std::string &imagePath,
|
|
155
|
+
double embeddingBufferSize) {
|
|
156
|
+
return Promise<std::vector<double>>::async(
|
|
157
|
+
[this, imagePath, embeddingBufferSize]() -> std::vector<double> {
|
|
158
|
+
std::lock_guard<std::mutex> lock(this->_modelMutex);
|
|
159
|
+
|
|
160
|
+
if (!this->_model) {
|
|
161
|
+
throw std::runtime_error("Cactus model is not initialized");
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
std::vector<float> embeddingBuffer(embeddingBufferSize);
|
|
165
|
+
size_t embeddingDim;
|
|
166
|
+
|
|
167
|
+
int result = cactus_image_embed(
|
|
168
|
+
this->_model, imagePath.c_str(), embeddingBuffer.data(),
|
|
169
|
+
embeddingBufferSize * sizeof(float), &embeddingDim);
|
|
170
|
+
|
|
171
|
+
if (result < 0) {
|
|
172
|
+
throw std::runtime_error("Cactus image embedding failed");
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
embeddingBuffer.resize(embeddingDim);
|
|
176
|
+
|
|
177
|
+
return std::vector<double>(embeddingBuffer.begin(),
|
|
178
|
+
embeddingBuffer.end());
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
std::shared_ptr<Promise<std::vector<double>>>
|
|
183
|
+
HybridCactus::audioEmbed(const std::string &audioPath,
|
|
184
|
+
double embeddingBufferSize) {
|
|
185
|
+
return Promise<std::vector<double>>::async(
|
|
186
|
+
[this, audioPath, embeddingBufferSize]() -> std::vector<double> {
|
|
187
|
+
std::lock_guard<std::mutex> lock(this->_modelMutex);
|
|
188
|
+
|
|
189
|
+
if (!this->_model) {
|
|
190
|
+
throw std::runtime_error("Cactus model is not initialized");
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
std::vector<float> embeddingBuffer(embeddingBufferSize);
|
|
194
|
+
size_t embeddingDim;
|
|
195
|
+
|
|
196
|
+
int result = cactus_audio_embed(
|
|
197
|
+
this->_model, audioPath.c_str(), embeddingBuffer.data(),
|
|
198
|
+
embeddingBufferSize * sizeof(float), &embeddingDim);
|
|
199
|
+
|
|
200
|
+
if (result < 0) {
|
|
201
|
+
throw std::runtime_error("Cactus audio embedding failed");
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
embeddingBuffer.resize(embeddingDim);
|
|
205
|
+
|
|
206
|
+
return std::vector<double>(embeddingBuffer.begin(),
|
|
207
|
+
embeddingBuffer.end());
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
|
|
106
211
|
std::shared_ptr<Promise<void>> HybridCactus::reset() {
|
|
107
212
|
return Promise<void>::async([this]() -> void {
|
|
108
213
|
std::lock_guard<std::mutex> lock(this->_modelMutex);
|
package/cpp/HybridCactus.hpp
CHANGED
|
@@ -23,9 +23,22 @@ public:
|
|
|
23
23
|
double /* tokenId */)>> &callback)
|
|
24
24
|
override;
|
|
25
25
|
|
|
26
|
+
std::shared_ptr<Promise<std::string>> transcribe(
|
|
27
|
+
const std::string &audioFilePath, const std::string &prompt,
|
|
28
|
+
double responseBufferSize, const std::optional<std::string> &optionsJson,
|
|
29
|
+
const std::optional<std::function<void(const std::string & /* token */,
|
|
30
|
+
double /* tokenId */)>> &callback)
|
|
31
|
+
override;
|
|
32
|
+
|
|
26
33
|
std::shared_ptr<Promise<std::vector<double>>>
|
|
27
34
|
embed(const std::string &text, double embeddingBufferSize) override;
|
|
28
35
|
|
|
36
|
+
std::shared_ptr<Promise<std::vector<double>>>
|
|
37
|
+
imageEmbed(const std::string &imagePath, double embeddingBufferSize) override;
|
|
38
|
+
|
|
39
|
+
std::shared_ptr<Promise<std::vector<double>>>
|
|
40
|
+
audioEmbed(const std::string &audioPath, double embeddingBufferSize) override;
|
|
41
|
+
|
|
29
42
|
std::shared_ptr<Promise<void>> reset() override;
|
|
30
43
|
|
|
31
44
|
std::shared_ptr<Promise<void>> stop() override;
|
package/cpp/cactus_ffi.h
CHANGED
|
@@ -33,6 +33,17 @@ CACTUS_FFI_EXPORT int cactus_complete(
|
|
|
33
33
|
void* user_data
|
|
34
34
|
);
|
|
35
35
|
|
|
36
|
+
CACTUS_FFI_EXPORT int cactus_transcribe(
|
|
37
|
+
cactus_model_t model,
|
|
38
|
+
const char* audio_file_path,
|
|
39
|
+
const char* prompt,
|
|
40
|
+
char* response_buffer,
|
|
41
|
+
size_t buffer_size,
|
|
42
|
+
const char* options_json,
|
|
43
|
+
cactus_token_callback callback,
|
|
44
|
+
void* user_data
|
|
45
|
+
);
|
|
46
|
+
|
|
36
47
|
|
|
37
48
|
CACTUS_FFI_EXPORT int cactus_embed(
|
|
38
49
|
cactus_model_t model,
|
|
@@ -42,6 +53,22 @@ CACTUS_FFI_EXPORT int cactus_embed(
|
|
|
42
53
|
size_t* embedding_dim
|
|
43
54
|
);
|
|
44
55
|
|
|
56
|
+
CACTUS_FFI_EXPORT int cactus_image_embed(
|
|
57
|
+
cactus_model_t model,
|
|
58
|
+
const char* image_path,
|
|
59
|
+
float* embeddings_buffer,
|
|
60
|
+
size_t buffer_size,
|
|
61
|
+
size_t* embedding_dim
|
|
62
|
+
);
|
|
63
|
+
|
|
64
|
+
CACTUS_FFI_EXPORT int cactus_audio_embed(
|
|
65
|
+
cactus_model_t model,
|
|
66
|
+
const char* audio_path,
|
|
67
|
+
float* embeddings_buffer,
|
|
68
|
+
size_t buffer_size,
|
|
69
|
+
size_t* embedding_dim
|
|
70
|
+
);
|
|
71
|
+
|
|
45
72
|
CACTUS_FFI_EXPORT void cactus_reset(cactus_model_t model);
|
|
46
73
|
|
|
47
74
|
CACTUS_FFI_EXPORT void cactus_stop(cactus_model_t model);
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
import NitroModules
|
|
3
|
+
import UIKit
|
|
4
|
+
|
|
5
|
+
class HybridCactusImage: HybridCactusImageSpec {
|
|
6
|
+
func base64(path: String) throws -> Promise<String> {
|
|
7
|
+
return Promise.async {
|
|
8
|
+
let fileURL = URL(fileURLWithPath: path)
|
|
9
|
+
|
|
10
|
+
if !FileManager.default.fileExists(atPath: fileURL.path) {
|
|
11
|
+
throw RuntimeError.error(withMessage: "No such file: \(path)")
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
let imageData = try Data(contentsOf: fileURL)
|
|
15
|
+
return imageData.base64EncodedString()
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
func resize(path: String, height: Double, width: Double, quality: Double) throws -> Promise<String> {
|
|
20
|
+
return Promise.async {
|
|
21
|
+
let fileURL = URL(fileURLWithPath: path)
|
|
22
|
+
|
|
23
|
+
if !FileManager.default.fileExists(atPath: fileURL.path) {
|
|
24
|
+
throw RuntimeError.error(withMessage: "No such file: \(path)")
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
guard let imageData = try? Data(contentsOf: fileURL),
|
|
28
|
+
let image = UIImage(data: imageData) else {
|
|
29
|
+
throw RuntimeError.error(withMessage: "Failed to load image from: \(path)")
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
let targetSize = CGSize(width: CGFloat(width), height: CGFloat(height))
|
|
33
|
+
let renderer = UIGraphicsImageRenderer(size: targetSize)
|
|
34
|
+
let resizedImage = renderer.image { context in
|
|
35
|
+
image.draw(in: CGRect(origin: .zero, size: targetSize))
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
guard let jpegData = resizedImage.jpegData(compressionQuality: CGFloat(quality)) else {
|
|
39
|
+
throw RuntimeError.error(withMessage: "Failed to compress resized image")
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
let cacheDir = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask)[0]
|
|
43
|
+
let fileName = "\(UUID().uuidString).jpg"
|
|
44
|
+
let outputURL = cacheDir.appendingPathComponent("cactus/images", isDirectory: true)
|
|
45
|
+
.appendingPathComponent(fileName)
|
|
46
|
+
|
|
47
|
+
try FileManager.default.createDirectory(at: outputURL.deletingLastPathComponent(), withIntermediateDirectories: true)
|
|
48
|
+
try jpegData.write(to: outputURL)
|
|
49
|
+
|
|
50
|
+
return outputURL.path
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
@@ -33,6 +33,17 @@ CACTUS_FFI_EXPORT int cactus_complete(
|
|
|
33
33
|
void* user_data
|
|
34
34
|
);
|
|
35
35
|
|
|
36
|
+
CACTUS_FFI_EXPORT int cactus_transcribe(
|
|
37
|
+
cactus_model_t model,
|
|
38
|
+
const char* audio_file_path,
|
|
39
|
+
const char* prompt,
|
|
40
|
+
char* response_buffer,
|
|
41
|
+
size_t buffer_size,
|
|
42
|
+
const char* options_json,
|
|
43
|
+
cactus_token_callback callback,
|
|
44
|
+
void* user_data
|
|
45
|
+
);
|
|
46
|
+
|
|
36
47
|
|
|
37
48
|
CACTUS_FFI_EXPORT int cactus_embed(
|
|
38
49
|
cactus_model_t model,
|
|
@@ -42,6 +53,22 @@ CACTUS_FFI_EXPORT int cactus_embed(
|
|
|
42
53
|
size_t* embedding_dim
|
|
43
54
|
);
|
|
44
55
|
|
|
56
|
+
CACTUS_FFI_EXPORT int cactus_image_embed(
|
|
57
|
+
cactus_model_t model,
|
|
58
|
+
const char* image_path,
|
|
59
|
+
float* embeddings_buffer,
|
|
60
|
+
size_t buffer_size,
|
|
61
|
+
size_t* embedding_dim
|
|
62
|
+
);
|
|
63
|
+
|
|
64
|
+
CACTUS_FFI_EXPORT int cactus_audio_embed(
|
|
65
|
+
cactus_model_t model,
|
|
66
|
+
const char* audio_path,
|
|
67
|
+
float* embeddings_buffer,
|
|
68
|
+
size_t buffer_size,
|
|
69
|
+
size_t* embedding_dim
|
|
70
|
+
);
|
|
71
|
+
|
|
45
72
|
CACTUS_FFI_EXPORT void cactus_reset(cactus_model_t model);
|
|
46
73
|
|
|
47
74
|
CACTUS_FFI_EXPORT void cactus_stop(cactus_model_t model);
|
|
@@ -7,11 +7,28 @@
|
|
|
7
7
|
#include <cstdint>
|
|
8
8
|
|
|
9
9
|
#include "../graph/graph.h"
|
|
10
|
+
|
|
11
|
+
#ifdef __clang__
|
|
12
|
+
#pragma clang diagnostic push
|
|
13
|
+
#pragma clang diagnostic ignored "-Wc99-extensions"
|
|
14
|
+
#pragma clang diagnostic ignored "-Wunused-parameter"
|
|
15
|
+
#elif defined(__GNUC__)
|
|
16
|
+
#pragma GCC diagnostic push
|
|
17
|
+
#pragma GCC diagnostic ignored "-Wpedantic"
|
|
18
|
+
#pragma GCC diagnostic ignored "-Wunused-parameter"
|
|
19
|
+
#endif
|
|
20
|
+
|
|
10
21
|
extern "C" {
|
|
11
22
|
#include "../../libs/stb/stb_image.h"
|
|
12
23
|
#include "../../libs/stb/stb_image_resize2.h"
|
|
13
24
|
}
|
|
14
25
|
|
|
26
|
+
#ifdef __clang__
|
|
27
|
+
#pragma clang diagnostic pop
|
|
28
|
+
#elif defined(__GNUC__)
|
|
29
|
+
#pragma GCC diagnostic pop
|
|
30
|
+
#endif
|
|
31
|
+
|
|
15
32
|
class CactusGraph;
|
|
16
33
|
|
|
17
34
|
namespace cactus {
|
|
@@ -68,7 +85,7 @@ struct Config {
|
|
|
68
85
|
float max_pixels_tolerance = 2.0f;
|
|
69
86
|
bool do_image_splitting = true;
|
|
70
87
|
|
|
71
|
-
enum class ModelType {QWEN = 0, GEMMA = 1, SMOL = 2, NOMIC = 3, LFM2 = 5, SIGLIP2 = 6};
|
|
88
|
+
enum class ModelType {QWEN = 0, GEMMA = 1, SMOL = 2, NOMIC = 3, LFM2 = 5, SIGLIP2 = 6, WHISPER = 7};
|
|
72
89
|
ModelType model_type = ModelType::QWEN;
|
|
73
90
|
|
|
74
91
|
enum class ModelVariant {DEFAULT = 0, VLM = 1, EXTRACT = 2, RAG = 3};
|
|
@@ -139,7 +156,7 @@ public:
|
|
|
139
156
|
void set_corpus_dir(const std::string& dir) { corpus_dir_ = dir; }
|
|
140
157
|
|
|
141
158
|
protected:
|
|
142
|
-
enum class ModelType { UNKNOWN, QWEN, GEMMA, LFM2, SMOL, BERT };
|
|
159
|
+
enum class ModelType { UNKNOWN, QWEN, GEMMA, LFM2, SMOL, BERT, WHISPER};
|
|
143
160
|
ModelType model_type_ = ModelType::UNKNOWN;
|
|
144
161
|
enum class ModelVariant { DEFAULT, VLM, EXTRACT, RAG};
|
|
145
162
|
ModelVariant model_variant_ = ModelVariant::DEFAULT;
|
|
@@ -365,28 +382,43 @@ public:
|
|
|
365
382
|
const std::vector<DebugNode>& get_debug_nodes() const;
|
|
366
383
|
|
|
367
384
|
virtual bool init(const std::string& model_folder, size_t context_size, const std::string& system_prompt = "", bool do_warmup = true);
|
|
385
|
+
|
|
368
386
|
virtual bool init(CactusGraph* external_graph, const std::string& model_folder, size_t context_size,
|
|
369
387
|
const std::string& system_prompt = "", bool do_warmup = true);
|
|
388
|
+
|
|
370
389
|
virtual uint32_t generate(const std::vector<uint32_t>& tokens, float temperature = -1.0f, float top_p = -1.0f,
|
|
371
390
|
size_t top_k = 0, const std::string& profile_file = "");
|
|
372
391
|
|
|
373
392
|
virtual uint32_t generate_with_images(const std::vector<uint32_t>& tokens, const std::vector<std::string>& image_paths,
|
|
374
393
|
float temperature = -1.0f, float top_p = -1.0f,
|
|
375
394
|
size_t top_k = 0, const std::string& profile_file = "");
|
|
395
|
+
|
|
396
|
+
virtual uint32_t generate_with_audio(const std::vector<uint32_t>& tokens, const std::vector<float>& mel_bins, float temperature = 0.0f, float top_p = 0.0f,
|
|
397
|
+
size_t top_k = 0, const std::string& profile_file = "");
|
|
376
398
|
|
|
377
399
|
std::vector<float> get_embeddings(const std::vector<uint32_t>& tokens, bool pooled = true, const std::string& profile_file = "");
|
|
400
|
+
|
|
401
|
+
virtual std::vector<float> get_image_embeddings(const std::string& image_path);
|
|
402
|
+
|
|
403
|
+
virtual std::vector<float> get_audio_embeddings(const std::vector<float>& mel_bins);
|
|
378
404
|
|
|
379
405
|
virtual void reset_cache() { kv_cache_.reset(); }
|
|
406
|
+
|
|
380
407
|
void set_cache_window(size_t window_size, size_t sink_size = 4) { kv_cache_.set_window_size(window_size, sink_size); }
|
|
381
408
|
|
|
382
409
|
void* graph_handle_;
|
|
383
410
|
|
|
384
411
|
protected:
|
|
385
412
|
virtual size_t forward(const std::vector<uint32_t>& tokens, bool use_cache = false) = 0;
|
|
413
|
+
|
|
414
|
+
virtual size_t forward(const std::vector<float>& mel_bins, const std::vector<uint32_t>& tokens, bool use_cache = false);
|
|
415
|
+
|
|
386
416
|
virtual void load_weights_to_graph(CactusGraph* gb) = 0;
|
|
417
|
+
|
|
387
418
|
virtual size_t build_attention(CactusGraph* gb, size_t normalized_input, uint32_t layer_idx,
|
|
388
419
|
ComputeBackend backend, bool use_cache = false, size_t position_offset = 0) = 0;
|
|
389
|
-
|
|
420
|
+
|
|
421
|
+
virtual size_t build_mlp(CactusGraph* gb, size_t normalized_h, uint32_t layer_idx,
|
|
390
422
|
ComputeBackend backend) const = 0;
|
|
391
423
|
virtual size_t build_transformer_block(CactusGraph* gb, size_t hidden, uint32_t layer_idx,
|
|
392
424
|
ComputeBackend backend, bool use_cache = false, size_t position_offset = 0) = 0;
|
|
@@ -8,6 +8,8 @@
|
|
|
8
8
|
#include <stdexcept>
|
|
9
9
|
#include <sstream>
|
|
10
10
|
#include <iomanip>
|
|
11
|
+
#include <fstream>
|
|
12
|
+
#include <iostream>
|
|
11
13
|
#include <filesystem>
|
|
12
14
|
#include <cctype>
|
|
13
15
|
|
|
@@ -177,8 +179,8 @@ inline void parse_options_json(const std::string& json,
|
|
|
177
179
|
float& temperature, float& top_p,
|
|
178
180
|
size_t& top_k, size_t& max_tokens,
|
|
179
181
|
std::vector<std::string>& stop_sequences) {
|
|
180
|
-
temperature =
|
|
181
|
-
top_p =
|
|
182
|
+
temperature = 0.0f;
|
|
183
|
+
top_p = 0.0f;
|
|
182
184
|
top_k = 0;
|
|
183
185
|
max_tokens = 100;
|
|
184
186
|
stop_sequences.clear();
|
|
@@ -233,15 +235,14 @@ inline std::string format_tools_for_prompt(const std::vector<ToolFunction>& tool
|
|
|
233
235
|
std::string formatted_tools_json;
|
|
234
236
|
for (size_t i = 0; i < tools.size(); i++) {
|
|
235
237
|
if (i > 0) formatted_tools_json += ",\n";
|
|
236
|
-
formatted_tools_json += "
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
formatted_tools_json += " \"description\": \"" + tools[i].description + "\"";
|
|
238
|
+
formatted_tools_json += "{\"type\":\"function\",\"function\":{\"name\":\""
|
|
239
|
+
+ tools[i].name
|
|
240
|
+
+ "\",\"description\":\""
|
|
241
|
+
+ tools[i].description + "\"";
|
|
241
242
|
if (tools[i].parameters.find("schema") != tools[i].parameters.end()) {
|
|
242
|
-
formatted_tools_json += ",\
|
|
243
|
+
formatted_tools_json += ",\"parameters\":" + tools[i].parameters.at("schema");
|
|
243
244
|
}
|
|
244
|
-
formatted_tools_json += "
|
|
245
|
+
formatted_tools_json += "}}";
|
|
245
246
|
}
|
|
246
247
|
return formatted_tools_json;
|
|
247
248
|
}
|
|
@@ -32,7 +32,7 @@ enum class OpType {
|
|
|
32
32
|
SUM, MEAN, VARIANCE, MIN, MAX,
|
|
33
33
|
RMS_NORM, ROPE, SOFTMAX, ATTENTION, CONV1D_CAUSAL, CONV1D_K3,
|
|
34
34
|
SCALAR_ADD, SCALAR_SUBTRACT, SCALAR_MULTIPLY, SCALAR_DIVIDE, SCALAR_EXP, SCALAR_SQRT, SCALAR_COS, SCALAR_SIN,
|
|
35
|
-
SILU, GELU,
|
|
35
|
+
SILU, GELU, GELU_ERF,
|
|
36
36
|
SAMPLE, CONCAT,
|
|
37
37
|
SCATTER_TOPK,
|
|
38
38
|
TOPK, LAYERNORM,
|
|
@@ -219,6 +219,7 @@ public:
|
|
|
219
219
|
|
|
220
220
|
size_t silu(size_t input);
|
|
221
221
|
size_t gelu(size_t input);
|
|
222
|
+
size_t gelu_erf(size_t input);
|
|
222
223
|
|
|
223
224
|
size_t matmul(size_t input1, size_t input2, bool pretransposed_rhs = false, ComputeBackend backend = ComputeBackend::CPU);
|
|
224
225
|
size_t transpose(size_t input, ComputeBackend backend = ComputeBackend::CPU);
|
|
@@ -236,6 +237,7 @@ public:
|
|
|
236
237
|
size_t gather(size_t embeddings, size_t indices);
|
|
237
238
|
size_t mmap_embeddings(const std::string& filename);
|
|
238
239
|
size_t mmap_weights(const std::string& filename);
|
|
240
|
+
size_t load_weights(const std::string& filename);
|
|
239
241
|
void set_quantization_scale(size_t node_id, float scale);
|
|
240
242
|
size_t embedding(const std::string& filename, size_t indices);
|
|
241
243
|
size_t embedding(size_t embedding_tensor, size_t indices);
|