cactus-react-native 1.0.2 → 1.2.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 (126) hide show
  1. package/README.md +378 -21
  2. package/android/src/main/java/com/margelo/nitro/cactus/HybridCactusCrypto.kt +23 -15
  3. package/android/src/main/java/com/margelo/nitro/cactus/HybridCactusDeviceInfo.kt +12 -9
  4. package/android/src/main/java/com/margelo/nitro/cactus/HybridCactusFileSystem.kt +42 -41
  5. package/android/src/main/java/com/margelo/nitro/cactus/HybridCactusImage.kt +81 -0
  6. package/android/src/main/jniLibs/arm64-v8a/libcactus.a +0 -0
  7. package/cpp/HybridCactus.cpp +105 -0
  8. package/cpp/HybridCactus.hpp +13 -0
  9. package/cpp/cactus_ffi.h +27 -0
  10. package/ios/HybridCactusImage.swift +53 -0
  11. package/ios/cactus.xcframework/ios-arm64/cactus.framework/Headers/cactus_ffi.h +27 -0
  12. package/ios/cactus.xcframework/ios-arm64/cactus.framework/Headers/engine.h +37 -5
  13. package/ios/cactus.xcframework/ios-arm64/cactus.framework/Headers/ffi_utils.h +10 -9
  14. package/ios/cactus.xcframework/ios-arm64/cactus.framework/Headers/graph.h +49 -7
  15. package/ios/cactus.xcframework/ios-arm64/cactus.framework/Headers/kernel.h +31 -0
  16. package/ios/cactus.xcframework/ios-arm64/cactus.framework/cactus +0 -0
  17. package/ios/cactus.xcframework/ios-arm64-simulator/cactus.framework/Headers/cactus_ffi.h +27 -0
  18. package/ios/cactus.xcframework/ios-arm64-simulator/cactus.framework/Headers/engine.h +37 -5
  19. package/ios/cactus.xcframework/ios-arm64-simulator/cactus.framework/Headers/ffi_utils.h +10 -9
  20. package/ios/cactus.xcframework/ios-arm64-simulator/cactus.framework/Headers/graph.h +49 -7
  21. package/ios/cactus.xcframework/ios-arm64-simulator/cactus.framework/Headers/kernel.h +31 -0
  22. package/ios/cactus.xcframework/ios-arm64-simulator/cactus.framework/cactus +0 -0
  23. package/lib/module/api/Database.js +23 -0
  24. package/lib/module/api/Database.js.map +1 -1
  25. package/lib/module/api/RemoteLM.js +201 -0
  26. package/lib/module/api/RemoteLM.js.map +1 -0
  27. package/lib/module/classes/CactusLM.js +52 -26
  28. package/lib/module/classes/CactusLM.js.map +1 -1
  29. package/lib/module/classes/CactusSTT.js +139 -0
  30. package/lib/module/classes/CactusSTT.js.map +1 -0
  31. package/lib/module/config/CactusConfig.js +4 -0
  32. package/lib/module/config/CactusConfig.js.map +1 -1
  33. package/lib/module/constants/packageVersion.js +1 -1
  34. package/lib/module/hooks/useCactusLM.js +33 -10
  35. package/lib/module/hooks/useCactusLM.js.map +1 -1
  36. package/lib/module/hooks/useCactusSTT.js +234 -0
  37. package/lib/module/hooks/useCactusSTT.js.map +1 -0
  38. package/lib/module/index.js +2 -0
  39. package/lib/module/index.js.map +1 -1
  40. package/lib/module/native/Cactus.js +50 -1
  41. package/lib/module/native/Cactus.js.map +1 -1
  42. package/lib/module/native/CactusFileSystem.js +2 -3
  43. package/lib/module/native/CactusFileSystem.js.map +1 -1
  44. package/lib/module/native/CactusImage.js +13 -0
  45. package/lib/module/native/CactusImage.js.map +1 -0
  46. package/lib/module/native/index.js +1 -0
  47. package/lib/module/native/index.js.map +1 -1
  48. package/lib/module/specs/CactusImage.nitro.js +4 -0
  49. package/lib/module/specs/CactusImage.nitro.js.map +1 -0
  50. package/lib/module/telemetry/Telemetry.js +53 -1
  51. package/lib/module/telemetry/Telemetry.js.map +1 -1
  52. package/lib/module/types/CactusSTT.js +2 -0
  53. package/lib/module/types/CactusSTT.js.map +1 -0
  54. package/lib/typescript/src/api/Database.d.ts +1 -0
  55. package/lib/typescript/src/api/Database.d.ts.map +1 -1
  56. package/lib/typescript/src/api/RemoteLM.d.ts +14 -0
  57. package/lib/typescript/src/api/RemoteLM.d.ts.map +1 -0
  58. package/lib/typescript/src/classes/CactusLM.d.ts +6 -4
  59. package/lib/typescript/src/classes/CactusLM.d.ts.map +1 -1
  60. package/lib/typescript/src/classes/CactusSTT.d.ts +26 -0
  61. package/lib/typescript/src/classes/CactusSTT.d.ts.map +1 -0
  62. package/lib/typescript/src/config/CactusConfig.d.ts +1 -0
  63. package/lib/typescript/src/config/CactusConfig.d.ts.map +1 -1
  64. package/lib/typescript/src/constants/packageVersion.d.ts +1 -1
  65. package/lib/typescript/src/hooks/useCactusLM.d.ts +4 -3
  66. package/lib/typescript/src/hooks/useCactusLM.d.ts.map +1 -1
  67. package/lib/typescript/src/hooks/useCactusSTT.d.ts +20 -0
  68. package/lib/typescript/src/hooks/useCactusSTT.d.ts.map +1 -0
  69. package/lib/typescript/src/index.d.ts +4 -1
  70. package/lib/typescript/src/index.d.ts.map +1 -1
  71. package/lib/typescript/src/native/Cactus.d.ts +9 -2
  72. package/lib/typescript/src/native/Cactus.d.ts.map +1 -1
  73. package/lib/typescript/src/native/CactusFileSystem.d.ts +1 -1
  74. package/lib/typescript/src/native/CactusFileSystem.d.ts.map +1 -1
  75. package/lib/typescript/src/native/CactusImage.d.ts +6 -0
  76. package/lib/typescript/src/native/CactusImage.d.ts.map +1 -0
  77. package/lib/typescript/src/native/index.d.ts +1 -0
  78. package/lib/typescript/src/native/index.d.ts.map +1 -1
  79. package/lib/typescript/src/specs/Cactus.nitro.d.ts +3 -0
  80. package/lib/typescript/src/specs/Cactus.nitro.d.ts.map +1 -1
  81. package/lib/typescript/src/specs/CactusImage.nitro.d.ts +9 -0
  82. package/lib/typescript/src/specs/CactusImage.nitro.d.ts.map +1 -0
  83. package/lib/typescript/src/telemetry/Telemetry.d.ts +5 -1
  84. package/lib/typescript/src/telemetry/Telemetry.d.ts.map +1 -1
  85. package/lib/typescript/src/types/CactusLM.d.ts +8 -5
  86. package/lib/typescript/src/types/CactusLM.d.ts.map +1 -1
  87. package/lib/typescript/src/types/CactusSTT.d.ts +37 -0
  88. package/lib/typescript/src/types/CactusSTT.d.ts.map +1 -0
  89. package/nitro.json +4 -0
  90. package/nitrogen/generated/android/c++/JHybridCactusImageSpec.cpp +81 -0
  91. package/nitrogen/generated/android/c++/JHybridCactusImageSpec.hpp +66 -0
  92. package/nitrogen/generated/android/cactus+autolinking.cmake +2 -0
  93. package/nitrogen/generated/android/cactusOnLoad.cpp +10 -0
  94. package/nitrogen/generated/android/kotlin/com/margelo/nitro/cactus/HybridCactusImageSpec.kt +62 -0
  95. package/nitrogen/generated/ios/Cactus-Swift-Cxx-Bridge.cpp +17 -0
  96. package/nitrogen/generated/ios/Cactus-Swift-Cxx-Bridge.hpp +17 -0
  97. package/nitrogen/generated/ios/Cactus-Swift-Cxx-Umbrella.hpp +5 -0
  98. package/nitrogen/generated/ios/CactusAutolinking.mm +8 -0
  99. package/nitrogen/generated/ios/CactusAutolinking.swift +15 -0
  100. package/nitrogen/generated/ios/c++/HybridCactusImageSpecSwift.cpp +11 -0
  101. package/nitrogen/generated/ios/c++/HybridCactusImageSpecSwift.hpp +85 -0
  102. package/nitrogen/generated/ios/swift/HybridCactusImageSpec.swift +58 -0
  103. package/nitrogen/generated/ios/swift/HybridCactusImageSpec_cxx.swift +158 -0
  104. package/nitrogen/generated/shared/c++/HybridCactusImageSpec.cpp +22 -0
  105. package/nitrogen/generated/shared/c++/HybridCactusImageSpec.hpp +64 -0
  106. package/nitrogen/generated/shared/c++/HybridCactusSpec.cpp +3 -0
  107. package/nitrogen/generated/shared/c++/HybridCactusSpec.hpp +3 -0
  108. package/package.json +1 -1
  109. package/src/api/Database.ts +27 -0
  110. package/src/api/RemoteLM.ts +273 -0
  111. package/src/classes/CactusLM.ts +72 -38
  112. package/src/classes/CactusSTT.ts +188 -0
  113. package/src/config/CactusConfig.ts +4 -0
  114. package/src/constants/packageVersion.ts +1 -1
  115. package/src/hooks/useCactusLM.ts +45 -17
  116. package/src/hooks/useCactusSTT.ts +285 -0
  117. package/src/index.tsx +14 -2
  118. package/src/native/Cactus.ts +94 -4
  119. package/src/native/CactusFileSystem.ts +2 -2
  120. package/src/native/CactusImage.ts +20 -0
  121. package/src/native/index.ts +1 -0
  122. package/src/specs/Cactus.nitro.ts +9 -0
  123. package/src/specs/CactusImage.nitro.ts +12 -0
  124. package/src/telemetry/Telemetry.ts +78 -1
  125. package/src/types/CactusLM.ts +9 -5
  126. 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
- return Promise.async {
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(path: String, content: String): Promise<Unit> {
32
- return Promise.async {
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
- return Promise.async {
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
- return Promise.async {
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, from: String, callback: ((progress: Double) -> Unit)?
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 = try {
86
- URL(from)
87
- } catch (_: Throwable) {
88
- throw Error("Invalid URL")
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 = (url.openConnection() as HttpURLConnection).apply {
96
- connectTimeout = 30_000
97
- readTimeout = 5 * 60_000
98
- instanceFollowRedirects = true
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 = floor(
133
- (downloaded.toDouble() / contentLength.toDouble()).coerceIn(
134
- 0.0, 1.0
135
- ) * 99
136
- ) / 100.0
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(zipFile: File, outDir: File) {
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
- return Promise.async {
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
+ }
@@ -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);
@@ -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;
@@ -302,7 +319,7 @@ private:
302
319
  };
303
320
 
304
321
  struct KVCache {
305
- static constexpr size_t DEFAULT_WINDOW_SIZE = 512;
322
+ static constexpr size_t DEFAULT_WINDOW_SIZE = 1024;
306
323
  static constexpr size_t DEFAULT_SINK_SIZE = 4;
307
324
 
308
325
  struct LayerCache {
@@ -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
- size_t top_k = 0, const std::string& profile_file = "");
390
+ size_t top_k = 0, const std::string& profile_file = "", bool prefill_only = false);
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
- virtual size_t build_mlp(CactusGraph* gb, size_t normalized_h, uint32_t layer_idx,
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 = -1.0f;
181
- top_p = -1.0f;
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 += " {\n";
237
- formatted_tools_json += " \"type\": \"function\",\n";
238
- formatted_tools_json += " \"function\": {\n";
239
- formatted_tools_json += " \"name\": \"" + tools[i].name + "\",\n";
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 += ",\n \"parameters\": " + tools[i].parameters.at("schema");
243
+ formatted_tools_json += ",\"parameters\":" + tools[i].parameters.at("schema");
243
244
  }
244
- formatted_tools_json += "\n }\n }";
245
+ formatted_tools_json += "}}";
245
246
  }
246
247
  return formatted_tools_json;
247
248
  }