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.
- 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 +37 -5
- 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 +49 -7
- 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 +37 -5
- 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 +49 -7
- 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 +139 -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 +26 -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 +188 -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
|
@@ -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
|
+
}
|
package/src/classes/CactusLM.ts
CHANGED
|
@@ -5,7 +5,8 @@ import type {
|
|
|
5
5
|
CactusLMCompleteResult,
|
|
6
6
|
CactusLMEmbedParams,
|
|
7
7
|
CactusLMEmbedResult,
|
|
8
|
-
|
|
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
|
|
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
|
|
58
|
-
await
|
|
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
|
-
|
|
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 (
|
|
123
|
-
|
|
124
|
-
|
|
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
|
-
|
|
177
|
-
|
|
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 +1 @@
|
|
|
1
|
-
export const packageVersion = '1.0
|
|
1
|
+
export const packageVersion = '1.2.0';
|