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
@@ -0,0 +1,273 @@
1
+ import { CactusConfig } from '../config/CactusConfig';
2
+ import { CactusImage } from '../native/CactusImage';
3
+ import type {
4
+ CactusLMCompleteResult,
5
+ Message,
6
+ CompleteOptions,
7
+ Tool,
8
+ } from '../types/CactusLM';
9
+
10
+ export class RemoteLM {
11
+ private static readonly completionsUrl =
12
+ 'https://openrouter.ai/api/v1/chat/completions';
13
+
14
+ private static readonly defaultModel = 'google/gemini-2.5-flash-lite';
15
+
16
+ public static async complete(
17
+ messages: Message[],
18
+ options?: CompleteOptions,
19
+ tools?: { type: 'function'; function: Tool }[],
20
+ callback?: (token: string) => void
21
+ ): Promise<CactusLMCompleteResult> {
22
+ if (!CactusConfig.cactusToken) {
23
+ throw new Error('cactusToken is required for hybrid completions');
24
+ }
25
+
26
+ const payload = JSON.stringify({
27
+ model: this.defaultModel,
28
+ messages: await this.transformMessages(messages),
29
+ tools,
30
+ temperature: options?.temperature,
31
+ top_p: options?.topP,
32
+ top_k: options?.topK,
33
+ max_tokens: options?.maxTokens,
34
+ stop: options?.stopSequences,
35
+ stream: !!callback,
36
+ });
37
+
38
+ return callback
39
+ ? await this.streamXHR(payload, callback)
40
+ : await this.nonStreamFetch(payload);
41
+ }
42
+
43
+ private static getMimeType(filePath: string): string {
44
+ const extension = filePath.toLowerCase().split('.').pop();
45
+ switch (extension) {
46
+ case 'jpg':
47
+ case 'jpeg':
48
+ return 'image/jpeg';
49
+ case 'png':
50
+ return 'image/png';
51
+ case 'gif':
52
+ return 'image/gif';
53
+ case 'webp':
54
+ return 'image/webp';
55
+ default:
56
+ throw new Error(`Unsupported image format: .${extension}`);
57
+ }
58
+ }
59
+
60
+ private static async transformMessages(messages: Message[]) {
61
+ const transformedMessages = [];
62
+
63
+ for (const message of messages) {
64
+ const content: {
65
+ type: string;
66
+ text?: string;
67
+ image_url?: { url: string };
68
+ }[] = [];
69
+
70
+ if (message.content) {
71
+ content.push({
72
+ type: 'text',
73
+ text: message.content,
74
+ });
75
+ }
76
+
77
+ if (message.images) {
78
+ for (const image of message.images) {
79
+ const imagePath = image.replace('file://', '');
80
+ const mimeType = this.getMimeType(imagePath);
81
+ const base64Data = await CactusImage.base64(imagePath);
82
+
83
+ content.push({
84
+ type: 'image_url',
85
+ image_url: {
86
+ url: `data:${mimeType};base64,${base64Data}`,
87
+ },
88
+ });
89
+ }
90
+ }
91
+
92
+ transformedMessages.push({ role: message.role, content });
93
+ }
94
+
95
+ return transformedMessages;
96
+ }
97
+
98
+ private static streamXHR(
99
+ payload: string,
100
+ callback: (token: string) => void
101
+ ): Promise<CactusLMCompleteResult> {
102
+ return new Promise((resolve, reject) => {
103
+ const xhr = new XMLHttpRequest();
104
+
105
+ xhr.timeout = 3 * 60 * 1000; // 3 minutes
106
+ xhr.ontimeout = () =>
107
+ reject(new Error('Remote streaming completion timed out'));
108
+
109
+ xhr.open('POST', this.completionsUrl);
110
+ xhr.setRequestHeader(
111
+ 'Authorization',
112
+ `Bearer ${CactusConfig.cactusToken}`
113
+ );
114
+ xhr.setRequestHeader('HTTP-Referer', 'https://cactuscompute.com');
115
+ xhr.setRequestHeader('X-Title', 'Cactus React Native SDK');
116
+ xhr.setRequestHeader('Content-Type', 'application/json');
117
+
118
+ const startTime = performance.now();
119
+ let lastIndex = 0;
120
+ let buffer = '';
121
+ let response = '';
122
+ let toolCalls: { name: string; arguments: string }[] | undefined;
123
+ let timeToFirstTokenMs = 0;
124
+ let prefillTokens = 0;
125
+ let decodeTokens = 0;
126
+ let totalTokens = 0;
127
+
128
+ xhr.onprogress = () => {
129
+ const chunk = xhr.responseText.substring(lastIndex);
130
+ lastIndex = xhr.responseText.length;
131
+
132
+ buffer += chunk;
133
+
134
+ const lines = buffer.split('\n');
135
+ buffer = lines.pop() || '';
136
+
137
+ for (const line of lines) {
138
+ if (!line.startsWith('data: ')) {
139
+ continue;
140
+ }
141
+
142
+ const jsonStr = line.slice(6).trim();
143
+ if (jsonStr === '[DONE]') {
144
+ continue;
145
+ }
146
+
147
+ try {
148
+ const data = JSON.parse(jsonStr);
149
+
150
+ if (timeToFirstTokenMs === 0) {
151
+ timeToFirstTokenMs = performance.now() - startTime;
152
+ }
153
+
154
+ const toolCallChunks = data?.choices?.[0]?.delta?.tool_calls;
155
+ if (toolCallChunks) {
156
+ if (!toolCalls) {
157
+ toolCalls = [];
158
+ }
159
+
160
+ for (const toolCallChunk of toolCallChunks) {
161
+ const index = toolCallChunk.index;
162
+
163
+ if (!toolCalls[index]) {
164
+ toolCalls[index] = { name: '', arguments: '' };
165
+ }
166
+
167
+ if (toolCallChunk.function?.name) {
168
+ toolCalls[index].name = toolCallChunk.function.name;
169
+ }
170
+
171
+ if (toolCallChunk.function?.arguments) {
172
+ toolCalls[index].arguments +=
173
+ toolCallChunk.function.arguments;
174
+ }
175
+ }
176
+ }
177
+
178
+ const content = data?.choices?.[0]?.delta?.content;
179
+ if (content) {
180
+ response += content;
181
+ callback(content);
182
+ }
183
+
184
+ if (data?.usage) {
185
+ prefillTokens = data.usage.prompt_tokens;
186
+ decodeTokens = data.usage.completion_tokens;
187
+ totalTokens = data.usage.total_tokens;
188
+ }
189
+ } catch {}
190
+ }
191
+ };
192
+
193
+ xhr.onload = () => {
194
+ const totalTimeMs = performance.now() - startTime;
195
+ const functionCalls = toolCalls?.map((toolCall) => ({
196
+ name: toolCall.name,
197
+ arguments: JSON.parse(toolCall.arguments) as { [key: string]: any },
198
+ }));
199
+
200
+ resolve({
201
+ success: true,
202
+ response,
203
+ functionCalls,
204
+ timeToFirstTokenMs,
205
+ totalTimeMs,
206
+ tokensPerSecond: (decodeTokens * 1000) / totalTimeMs,
207
+ prefillTokens,
208
+ decodeTokens,
209
+ totalTokens,
210
+ });
211
+ };
212
+
213
+ xhr.onerror = () =>
214
+ reject(new Error('Remote streaming completion failed'));
215
+
216
+ xhr.send(payload);
217
+ });
218
+ }
219
+
220
+ private static async nonStreamFetch(
221
+ payload: string
222
+ ): Promise<CactusLMCompleteResult> {
223
+ const startTime = performance.now();
224
+
225
+ const request = await fetch(this.completionsUrl, {
226
+ method: 'POST',
227
+ headers: {
228
+ 'Authorization': `Bearer ${CactusConfig.cactusToken}`,
229
+ 'HTTP-Referer': 'https://cactuscompute.com',
230
+ 'X-Title': 'Cactus React Native SDK',
231
+ 'Content-Type': 'application/json',
232
+ },
233
+ body: payload,
234
+ });
235
+
236
+ if (!request.ok) {
237
+ throw new Error('Remote completion failed');
238
+ }
239
+
240
+ const data = await request.json();
241
+
242
+ const totalTimeMs = performance.now() - startTime;
243
+ const decodeTokens = data.usage.completion_tokens;
244
+
245
+ const toolCalls:
246
+ | {
247
+ function: {
248
+ name: string;
249
+ arguments: {
250
+ [key: string]: any;
251
+ };
252
+ };
253
+ }[]
254
+ | undefined = data.choices[0].message.tool_calls;
255
+
256
+ const functionCalls = toolCalls?.map((toolCall) => ({
257
+ name: toolCall.function.name,
258
+ arguments: toolCall.function.arguments,
259
+ }));
260
+
261
+ return {
262
+ success: true,
263
+ response: data.choices[0].message.content,
264
+ functionCalls,
265
+ timeToFirstTokenMs: totalTimeMs,
266
+ totalTimeMs,
267
+ tokensPerSecond: (decodeTokens * 1000) / totalTimeMs,
268
+ prefillTokens: data.usage.prompt_tokens,
269
+ decodeTokens,
270
+ totalTokens: data.usage.total_tokens,
271
+ };
272
+ }
273
+ }
@@ -5,7 +5,8 @@ import type {
5
5
  CactusLMCompleteResult,
6
6
  CactusLMEmbedParams,
7
7
  CactusLMEmbedResult,
8
- CactusLMGetModelsParams,
8
+ CactusLMImageEmbedParams,
9
+ CactusLMImageEmbedResult,
9
10
  CactusLMParams,
10
11
  } from '../types/CactusLM';
11
12
  import type { CactusModel } from '../types/CactusModel';
@@ -13,6 +14,7 @@ import { Telemetry } from '../telemetry/Telemetry';
13
14
  import { CactusConfig } from '../config/CactusConfig';
14
15
  import { Database } from '../api/Database';
15
16
  import { getErrorMessage } from '../utils/error';
17
+ import { RemoteLM } from '../api/RemoteLM';
16
18
 
17
19
  export class CactusLM {
18
20
  private readonly cactus = new Cactus();
@@ -30,11 +32,14 @@ export class CactusLM {
30
32
  private static readonly defaultCompleteOptions = {
31
33
  maxTokens: 512,
32
34
  };
35
+ private static readonly defaultCompleteMode = 'local';
33
36
  private static readonly defaultEmbedBufferSize = 2048;
34
37
 
35
- private static readonly modelsInfoPath = 'models/info.json';
38
+ private static cactusModelsCache: CactusModel[] | null = null;
36
39
 
37
40
  constructor({ model, contextSize, corpusDir }: CactusLMParams = {}) {
41
+ Telemetry.init(CactusConfig.telemetryToken);
42
+
38
43
  this.model = model ?? CactusLM.defaultModel;
39
44
  this.contextSize = contextSize ?? CactusLM.defaultContextSize;
40
45
  this.corpusDir = corpusDir;
@@ -54,8 +59,12 @@ export class CactusLM {
54
59
 
55
60
  this.isDownloading = true;
56
61
  try {
57
- await CactusFileSystem.downloadModel(this.model, onProgress);
58
- await this.getModels({ forceRefresh: true });
62
+ const model = await Database.getModel(this.model);
63
+ await CactusFileSystem.downloadModel(
64
+ this.model,
65
+ model.downloadUrl,
66
+ onProgress
67
+ );
59
68
  } finally {
60
69
  this.isDownloading = false;
61
70
  }
@@ -66,10 +75,6 @@ export class CactusLM {
66
75
  return;
67
76
  }
68
77
 
69
- if (!Telemetry.isInitialized()) {
70
- await Telemetry.init(CactusConfig.telemetryToken);
71
- }
72
-
73
78
  if (!(await CactusFileSystem.modelExists(this.model))) {
74
79
  throw new Error(`Model "${this.model}" is not downloaded`);
75
80
  }
@@ -91,25 +96,32 @@ export class CactusLM {
91
96
  options,
92
97
  tools,
93
98
  onToken,
99
+ mode,
94
100
  }: CactusLMCompleteParams): Promise<CactusLMCompleteResult> {
95
101
  if (this.isGenerating) {
96
102
  throw new Error('CactusLM is already generating');
97
103
  }
98
104
 
99
- await this.init();
100
-
101
105
  options = { ...CactusLM.defaultCompleteOptions, ...options };
106
+ const toolsInternal = tools?.map((tool) => ({
107
+ type: 'function' as const,
108
+ function: tool,
109
+ }));
110
+ mode = mode ?? CactusLM.defaultCompleteMode;
111
+
102
112
  const responseBufferSize =
103
113
  8 * (options.maxTokens ?? CactusLM.defaultCompleteOptions.maxTokens) +
104
114
  256;
105
115
 
106
- this.isGenerating = true;
107
116
  try {
117
+ await this.init();
118
+
119
+ this.isGenerating = true;
108
120
  const result = await this.cactus.complete(
109
121
  messages,
110
122
  responseBufferSize,
111
123
  options,
112
- tools,
124
+ toolsInternal,
113
125
  onToken
114
126
  );
115
127
  Telemetry.logCompletion(
@@ -119,9 +131,25 @@ export class CactusLM {
119
131
  result
120
132
  );
121
133
  return result;
122
- } catch (error) {
123
- Telemetry.logCompletion(this.model, false, getErrorMessage(error));
124
- throw error;
134
+ } catch (localError) {
135
+ if (mode === 'local') {
136
+ Telemetry.logCompletion(this.model, false, getErrorMessage(localError));
137
+ throw localError;
138
+ }
139
+
140
+ Telemetry.logCompletion(
141
+ this.model,
142
+ false,
143
+ `Local completion error: ${getErrorMessage(localError)}. Falling back to remote completion.`
144
+ );
145
+
146
+ try {
147
+ return RemoteLM.complete(messages, options, toolsInternal, onToken);
148
+ } catch (remoteError) {
149
+ throw new Error(
150
+ `Remote completion error: ${getErrorMessage(remoteError)}`
151
+ );
152
+ }
125
153
  } finally {
126
154
  this.isGenerating = false;
127
155
  }
@@ -152,6 +180,31 @@ export class CactusLM {
152
180
  }
153
181
  }
154
182
 
183
+ public async imageEmbed({
184
+ imagePath,
185
+ }: CactusLMImageEmbedParams): Promise<CactusLMImageEmbedResult> {
186
+ if (this.isGenerating) {
187
+ throw new Error('CactusLM is already generating');
188
+ }
189
+
190
+ await this.init();
191
+
192
+ this.isGenerating = true;
193
+ try {
194
+ const embedding = await this.cactus.imageEmbed(
195
+ imagePath,
196
+ CactusLM.defaultEmbedBufferSize
197
+ );
198
+ Telemetry.logImageEmbedding(this.model, true);
199
+ return { embedding };
200
+ } catch (error) {
201
+ Telemetry.logImageEmbedding(this.model, false, getErrorMessage(error));
202
+ throw error;
203
+ } finally {
204
+ this.isGenerating = false;
205
+ }
206
+ }
207
+
155
208
  public stop(): Promise<void> {
156
209
  return this.cactus.stop();
157
210
  }
@@ -172,34 +225,15 @@ export class CactusLM {
172
225
  this.isInitialized = false;
173
226
  }
174
227
 
175
- public async getModels({
176
- forceRefresh = false,
177
- }: CactusLMGetModelsParams = {}): Promise<CactusModel[]> {
178
- if (
179
- !forceRefresh &&
180
- (await CactusFileSystem.fileExists(CactusLM.modelsInfoPath))
181
- ) {
182
- try {
183
- return JSON.parse(
184
- await CactusFileSystem.readFile(CactusLM.modelsInfoPath)
185
- );
186
- } catch {
187
- // Delete corrupted models info
188
- await CactusFileSystem.deleteFile(CactusLM.modelsInfoPath);
189
- }
228
+ public async getModels(): Promise<CactusModel[]> {
229
+ if (CactusLM.cactusModelsCache) {
230
+ return CactusLM.cactusModelsCache;
190
231
  }
191
-
192
232
  const models = await Database.getModels();
193
-
194
233
  for (const model of models) {
195
234
  model.isDownloaded = await CactusFileSystem.modelExists(model.slug);
196
235
  }
197
-
198
- await CactusFileSystem.writeFile(
199
- CactusLM.modelsInfoPath,
200
- JSON.stringify(models)
201
- );
202
-
236
+ CactusLM.cactusModelsCache = models;
203
237
  return models;
204
238
  }
205
239
  }
@@ -0,0 +1,188 @@
1
+ import { Cactus, CactusFileSystem } from '../native';
2
+ import type {
3
+ CactusSTTDownloadParams,
4
+ CactusSTTTranscribeParams,
5
+ CactusSTTTranscribeResult,
6
+ CactusSTTParams,
7
+ CactusSTTAudioEmbedParams,
8
+ CactusSTTAudioEmbedResult,
9
+ } from '../types/CactusSTT';
10
+ import type { CactusModel } from '../types/CactusModel';
11
+ import { Telemetry } from '../telemetry/Telemetry';
12
+ import { CactusConfig } from '../config/CactusConfig';
13
+ import { Database } from '../api/Database';
14
+ import { getErrorMessage } from '../utils/error';
15
+
16
+ export class CactusSTT {
17
+ private readonly cactus = new Cactus();
18
+
19
+ private readonly model: string;
20
+ private readonly contextSize: number;
21
+
22
+ private isDownloading = false;
23
+ private isInitialized = false;
24
+ private isGenerating = false;
25
+
26
+ private static readonly defaultModel = 'whisper-small';
27
+ private static readonly defaultContextSize = 2048;
28
+ private static readonly defaultPrompt =
29
+ '<|startoftranscript|><|en|><|transcribe|><|notimestamps|>';
30
+ private static readonly defaultTranscribeOptions = {
31
+ maxTokens: 512,
32
+ };
33
+ private static readonly defaultEmbedBufferSize = 4096;
34
+
35
+ private static cactusModelsCache: CactusModel[] | null = null;
36
+
37
+ constructor({ model, contextSize }: CactusSTTParams = {}) {
38
+ Telemetry.init(CactusConfig.telemetryToken);
39
+
40
+ this.model = model ?? CactusSTT.defaultModel;
41
+ this.contextSize = contextSize ?? CactusSTT.defaultContextSize;
42
+ }
43
+
44
+ public async download({
45
+ onProgress,
46
+ }: CactusSTTDownloadParams = {}): Promise<void> {
47
+ if (this.isDownloading) {
48
+ throw new Error('CactusSTT is already downloading');
49
+ }
50
+
51
+ if (await CactusFileSystem.modelExists(this.model)) {
52
+ onProgress?.(1.0);
53
+ return;
54
+ }
55
+
56
+ this.isDownloading = true;
57
+ try {
58
+ await CactusFileSystem.downloadModel(
59
+ this.model,
60
+ `https://vlqqczxwyaodtcdmdmlw.supabase.co/storage/v1/object/public/voice-models/${this.model}.zip`,
61
+ onProgress
62
+ );
63
+ } finally {
64
+ this.isDownloading = false;
65
+ }
66
+ }
67
+
68
+ public async init(): Promise<void> {
69
+ if (this.isInitialized) {
70
+ return;
71
+ }
72
+
73
+ if (!(await CactusFileSystem.modelExists(this.model))) {
74
+ throw new Error(`Model "${this.model}" is not downloaded`);
75
+ }
76
+
77
+ const modelPath = await CactusFileSystem.getModelPath(this.model);
78
+
79
+ try {
80
+ await this.cactus.init(modelPath, this.contextSize);
81
+ Telemetry.logInit(this.model, true);
82
+ this.isInitialized = true;
83
+ } catch (error) {
84
+ Telemetry.logInit(this.model, false, getErrorMessage(error));
85
+ throw error;
86
+ }
87
+ }
88
+
89
+ public async transcribe({
90
+ audioFilePath,
91
+ prompt,
92
+ options,
93
+ onToken,
94
+ }: CactusSTTTranscribeParams): Promise<CactusSTTTranscribeResult> {
95
+ if (this.isGenerating) {
96
+ throw new Error('CactusSTT is already generating');
97
+ }
98
+
99
+ await this.init();
100
+
101
+ prompt = prompt ?? CactusSTT.defaultPrompt;
102
+ options = { ...CactusSTT.defaultTranscribeOptions, ...options };
103
+
104
+ const responseBufferSize =
105
+ 8 * (options.maxTokens ?? CactusSTT.defaultTranscribeOptions.maxTokens) +
106
+ 256;
107
+
108
+ this.isGenerating = true;
109
+ try {
110
+ const result = await this.cactus.transcribe(
111
+ audioFilePath,
112
+ prompt,
113
+ responseBufferSize,
114
+ options,
115
+ onToken
116
+ );
117
+ Telemetry.logTranscribe(
118
+ this.model,
119
+ result.success,
120
+ result.success ? undefined : result.response,
121
+ result
122
+ );
123
+ return result;
124
+ } catch (error) {
125
+ Telemetry.logTranscribe(this.model, false, getErrorMessage(error));
126
+ throw error;
127
+ } finally {
128
+ this.isGenerating = false;
129
+ }
130
+ }
131
+
132
+ public async audioEmbed({
133
+ audioPath,
134
+ }: CactusSTTAudioEmbedParams): Promise<CactusSTTAudioEmbedResult> {
135
+ if (this.isGenerating) {
136
+ throw new Error('CactusSTT is already generating');
137
+ }
138
+
139
+ await this.init();
140
+
141
+ this.isGenerating = true;
142
+ try {
143
+ const embedding = await this.cactus.audioEmbed(
144
+ audioPath,
145
+ CactusSTT.defaultEmbedBufferSize
146
+ );
147
+ Telemetry.logAudioEmbedding(this.model, true);
148
+ return { embedding };
149
+ } catch (error) {
150
+ Telemetry.logAudioEmbedding(this.model, false, getErrorMessage(error));
151
+ throw error;
152
+ } finally {
153
+ this.isGenerating = false;
154
+ }
155
+ }
156
+
157
+ public stop(): Promise<void> {
158
+ return this.cactus.stop();
159
+ }
160
+
161
+ public async reset(): Promise<void> {
162
+ await this.stop();
163
+ return this.cactus.reset();
164
+ }
165
+
166
+ public async destroy(): Promise<void> {
167
+ if (!this.isInitialized) {
168
+ return;
169
+ }
170
+
171
+ await this.stop();
172
+ await this.cactus.destroy();
173
+
174
+ this.isInitialized = false;
175
+ }
176
+
177
+ public async getModels(): Promise<CactusModel[]> {
178
+ if (CactusSTT.cactusModelsCache) {
179
+ return CactusSTT.cactusModelsCache;
180
+ }
181
+ const models = await Database.getModels();
182
+ for (const model of models) {
183
+ model.isDownloaded = await CactusFileSystem.modelExists(model.slug);
184
+ }
185
+ CactusSTT.cactusModelsCache = models;
186
+ return models;
187
+ }
188
+ }
@@ -1,4 +1,8 @@
1
1
  export class CactusConfig {
2
+ // Telemetry
2
3
  public static telemetryToken?: string;
3
4
  public static isTelemetryEnabled: boolean = true;
5
+
6
+ // Hybrid mode
7
+ public static cactusToken?: string;
4
8
  }
@@ -1 +1 @@
1
- export const packageVersion = '1.0.2';
1
+ export const packageVersion = '1.2.0';