cactus-react-native 0.2.10 → 0.2.11
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 +52 -776
- package/android/src/main/CMakeLists.txt +2 -1
- package/android/src/main/java/com/cactus/CactusPackage.java +5 -5
- package/android/src/main/java/com/cactus/LlamaContext.java +1 -67
- package/android/src/main/jniLibs/arm64-v8a/libcactus.so +0 -0
- package/android/src/main/jniLibs/arm64-v8a/libcactus_v8.so +0 -0
- package/android/src/main/jniLibs/arm64-v8a/libcactus_v8_2.so +0 -0
- package/android/src/main/jniLibs/arm64-v8a/libcactus_v8_2_dotprod.so +0 -0
- package/android/src/main/jniLibs/arm64-v8a/libcactus_v8_2_dotprod_i8mm.so +0 -0
- package/android/src/main/jniLibs/arm64-v8a/libcactus_v8_2_i8mm.so +0 -0
- package/android/src/newarch/java/com/cactus/CactusModule.java +0 -2
- package/ios/cactus.xcframework/ios-arm64/cactus.framework/Info.plist +0 -0
- package/ios/cactus.xcframework/ios-arm64/cactus.framework/cactus +0 -0
- package/ios/cactus.xcframework/ios-arm64_x86_64-simulator/cactus.framework/Info.plist +0 -0
- package/ios/cactus.xcframework/ios-arm64_x86_64-simulator/cactus.framework/_CodeSignature/CodeResources +1 -1
- package/ios/cactus.xcframework/ios-arm64_x86_64-simulator/cactus.framework/cactus +0 -0
- package/ios/cactus.xcframework/tvos-arm64_x86_64-simulator/cactus.framework/Info.plist +0 -0
- package/ios/cactus.xcframework/tvos-arm64_x86_64-simulator/cactus.framework/_CodeSignature/CodeResources +1 -1
- package/ios/cactus.xcframework/tvos-arm64_x86_64-simulator/cactus.framework/cactus +0 -0
- package/lib/commonjs/index.js.map +1 -1
- package/lib/commonjs/projectId.js +2 -1
- package/lib/commonjs/projectId.js.map +1 -1
- package/lib/commonjs/tts.js +124 -12
- package/lib/commonjs/tts.js.map +1 -1
- package/lib/module/index.js.map +1 -1
- package/lib/module/projectId.js +2 -1
- package/lib/module/projectId.js.map +1 -1
- package/lib/module/tts.js +123 -12
- package/lib/module/tts.js.map +1 -1
- package/lib/typescript/index.d.ts +1 -1
- package/lib/typescript/index.d.ts.map +1 -1
- package/lib/typescript/projectId.d.ts +1 -1
- package/lib/typescript/projectId.d.ts.map +1 -1
- package/lib/typescript/tts.d.ts +46 -2
- package/lib/typescript/tts.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/index.ts +1 -0
- package/src/tts.ts +209 -15
package/README.md
CHANGED
|
@@ -1,828 +1,104 @@
|
|
|
1
|
-
|
|
1
|
+

|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Official React-Native plugin for Cactus, a framework for deploying LLM/VLM/TTS models locally in your app. Requires iOS 12.0+, Android API 24+ and Yarn. For iOS apps, ensure you have cocoapods or install with `brew install cocoapods`. For Android apps, you need Java 17 installed. Expo is strongly recommended.
|
|
4
4
|
|
|
5
|
-
##
|
|
5
|
+
## Resources
|
|
6
|
+
[](https://github.com/cactus-compute/cactus) [](https://huggingface.co/Cactus-Compute/models?sort=downloads) [](https://discord.gg/bNurx3AXTJ) [](https://cactuscompute.com/docs/react-native)
|
|
6
7
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
}
|
|
8
|
+
## Installation
|
|
9
|
+
Execute the following command in your project terminal:
|
|
10
|
+
```bash
|
|
11
|
+
npm install cactus-react-native
|
|
12
|
+
# or
|
|
13
|
+
yarn add cactus-react-native
|
|
14
14
|
```
|
|
15
|
+
*N/B*: To build locally or use this repo, see instructions in `example/README.md`
|
|
15
16
|
|
|
16
|
-
|
|
17
|
-
- iOS: `cd ios && npx pod-install`
|
|
18
|
-
- Android: Ensure `minSdkVersion` 24+
|
|
19
|
-
|
|
20
|
-
## Quick Start
|
|
21
|
-
|
|
17
|
+
## Text Completion
|
|
22
18
|
```typescript
|
|
23
19
|
import { CactusLM } from 'cactus-react-native';
|
|
24
|
-
import RNFS from 'react-native-fs';
|
|
25
|
-
|
|
26
|
-
const modelPath = `${RNFS.DocumentDirectoryPath}/model.gguf`;
|
|
27
20
|
|
|
28
21
|
const { lm, error } = await CactusLM.init({
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
n_threads: 4,
|
|
22
|
+
model: '/path/to/model.gguf', // this is a local model file inside the app sandbox
|
|
23
|
+
n_ctx: 2048,
|
|
32
24
|
});
|
|
33
25
|
|
|
34
|
-
if (error) throw error;
|
|
35
|
-
|
|
36
26
|
const messages = [{ role: 'user', content: 'Hello!' }];
|
|
37
|
-
const
|
|
38
|
-
|
|
39
|
-
lm.release();
|
|
40
|
-
```
|
|
41
|
-
|
|
42
|
-
## Streaming Chat
|
|
43
|
-
|
|
44
|
-
```typescript
|
|
45
|
-
import React, { useState, useEffect } from 'react';
|
|
46
|
-
import { View, Text, TextInput, TouchableOpacity, ScrollView, ActivityIndicator } from 'react-native';
|
|
47
|
-
import { CactusLM } from 'cactus-react-native';
|
|
48
|
-
import RNFS from 'react-native-fs';
|
|
49
|
-
|
|
50
|
-
interface Message {
|
|
51
|
-
role: 'user' | 'assistant';
|
|
52
|
-
content: string;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
export default function ChatScreen() {
|
|
56
|
-
const [lm, setLM] = useState<CactusLM | null>(null);
|
|
57
|
-
const [messages, setMessages] = useState<Message[]>([]);
|
|
58
|
-
const [input, setInput] = useState('');
|
|
59
|
-
const [isLoading, setIsLoading] = useState(true);
|
|
60
|
-
const [isGenerating, setIsGenerating] = useState(false);
|
|
61
|
-
|
|
62
|
-
useEffect(() => {
|
|
63
|
-
initializeModel();
|
|
64
|
-
return () => {
|
|
65
|
-
lm?.release();
|
|
66
|
-
};
|
|
67
|
-
}, []);
|
|
68
|
-
|
|
69
|
-
const initializeModel = async () => {
|
|
70
|
-
try {
|
|
71
|
-
const modelUrl = 'https://huggingface.co/Cactus-Compute/Qwen3-600m-Instruct-GGUF/resolve/main/Qwen3-0.6B-Q8_0.gguf';
|
|
72
|
-
const modelPath = await downloadModel(modelUrl, 'qwen-600m.gguf');
|
|
73
|
-
|
|
74
|
-
const { lm: model, error } = await CactusLM.init({
|
|
75
|
-
model: modelPath,
|
|
76
|
-
n_ctx: 2048,
|
|
77
|
-
n_threads: 4,
|
|
78
|
-
n_gpu_layers: 99,
|
|
79
|
-
});
|
|
80
|
-
|
|
81
|
-
if (error) throw error;
|
|
82
|
-
setLM(model);
|
|
83
|
-
} catch (error) {
|
|
84
|
-
console.error('Failed to initialize model:', error);
|
|
85
|
-
} finally {
|
|
86
|
-
setIsLoading(false);
|
|
87
|
-
}
|
|
88
|
-
};
|
|
89
|
-
|
|
90
|
-
const downloadModel = async (url: string, filename: string): Promise<string> => {
|
|
91
|
-
const path = `${RNFS.DocumentDirectoryPath}/${filename}`;
|
|
92
|
-
|
|
93
|
-
if (await RNFS.exists(path)) return path;
|
|
94
|
-
|
|
95
|
-
console.log('Downloading model...');
|
|
96
|
-
await RNFS.downloadFile({
|
|
97
|
-
fromUrl: url,
|
|
98
|
-
toFile: path,
|
|
99
|
-
progress: (res) => {
|
|
100
|
-
const progress = res.bytesWritten / res.contentLength;
|
|
101
|
-
console.log(`Download progress: ${(progress * 100).toFixed(1)}%`);
|
|
102
|
-
},
|
|
103
|
-
}).promise;
|
|
104
|
-
|
|
105
|
-
return path;
|
|
106
|
-
};
|
|
107
|
-
|
|
108
|
-
const sendMessage = async () => {
|
|
109
|
-
if (!lm || !input.trim() || isGenerating) return;
|
|
110
|
-
|
|
111
|
-
const userMessage: Message = { role: 'user', content: input.trim() };
|
|
112
|
-
const newMessages = [...messages, userMessage];
|
|
113
|
-
setMessages([...newMessages, { role: 'assistant', content: '' }]);
|
|
114
|
-
setInput('');
|
|
115
|
-
setIsGenerating(true);
|
|
116
|
-
|
|
117
|
-
try {
|
|
118
|
-
let response = '';
|
|
119
|
-
await lm.completion(newMessages, {
|
|
120
|
-
n_predict: 200,
|
|
121
|
-
temperature: 0.7,
|
|
122
|
-
stop: ['</s>', '<|end|>'],
|
|
123
|
-
}, (token) => {
|
|
124
|
-
response += token.token;
|
|
125
|
-
setMessages(prev => [
|
|
126
|
-
...prev.slice(0, -1),
|
|
127
|
-
{ role: 'assistant', content: response }
|
|
128
|
-
]);
|
|
129
|
-
});
|
|
130
|
-
} catch (error) {
|
|
131
|
-
console.error('Generation failed:', error);
|
|
132
|
-
setMessages(prev => [
|
|
133
|
-
...prev.slice(0, -1),
|
|
134
|
-
{ role: 'assistant', content: 'Error generating response' }
|
|
135
|
-
]);
|
|
136
|
-
} finally {
|
|
137
|
-
setIsGenerating(false);
|
|
138
|
-
}
|
|
139
|
-
};
|
|
140
|
-
|
|
141
|
-
if (isLoading) {
|
|
142
|
-
return (
|
|
143
|
-
<View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}>
|
|
144
|
-
<ActivityIndicator size="large" />
|
|
145
|
-
<Text style={{ marginTop: 16 }}>Loading model...</Text>
|
|
146
|
-
</View>
|
|
147
|
-
);
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
return (
|
|
151
|
-
<View style={{ flex: 1, backgroundColor: '#f5f5f5' }}>
|
|
152
|
-
<ScrollView style={{ flex: 1, padding: 16 }}>
|
|
153
|
-
{messages.map((msg, index) => (
|
|
154
|
-
<View
|
|
155
|
-
key={index}
|
|
156
|
-
style={{
|
|
157
|
-
backgroundColor: msg.role === 'user' ? '#007AFF' : '#ffffff',
|
|
158
|
-
padding: 12,
|
|
159
|
-
marginVertical: 4,
|
|
160
|
-
borderRadius: 12,
|
|
161
|
-
alignSelf: msg.role === 'user' ? 'flex-end' : 'flex-start',
|
|
162
|
-
maxWidth: '80%',
|
|
163
|
-
shadowColor: '#000',
|
|
164
|
-
shadowOffset: { width: 0, height: 1 },
|
|
165
|
-
shadowOpacity: 0.2,
|
|
166
|
-
shadowRadius: 2,
|
|
167
|
-
elevation: 2,
|
|
168
|
-
}}
|
|
169
|
-
>
|
|
170
|
-
<Text style={{
|
|
171
|
-
color: msg.role === 'user' ? '#ffffff' : '#000000',
|
|
172
|
-
fontSize: 16,
|
|
173
|
-
}}>
|
|
174
|
-
{msg.content}
|
|
175
|
-
</Text>
|
|
176
|
-
</View>
|
|
177
|
-
))}
|
|
178
|
-
</ScrollView>
|
|
179
|
-
|
|
180
|
-
<View style={{
|
|
181
|
-
flexDirection: 'row',
|
|
182
|
-
padding: 16,
|
|
183
|
-
backgroundColor: '#ffffff',
|
|
184
|
-
borderTopWidth: 1,
|
|
185
|
-
borderTopColor: '#e0e0e0',
|
|
186
|
-
}}>
|
|
187
|
-
<TextInput
|
|
188
|
-
style={{
|
|
189
|
-
flex: 1,
|
|
190
|
-
borderWidth: 1,
|
|
191
|
-
borderColor: '#e0e0e0',
|
|
192
|
-
borderRadius: 20,
|
|
193
|
-
paddingHorizontal: 16,
|
|
194
|
-
paddingVertical: 10,
|
|
195
|
-
fontSize: 16,
|
|
196
|
-
backgroundColor: '#f8f8f8',
|
|
197
|
-
}}
|
|
198
|
-
value={input}
|
|
199
|
-
onChangeText={setInput}
|
|
200
|
-
placeholder="Type a message..."
|
|
201
|
-
multiline
|
|
202
|
-
onSubmitEditing={sendMessage}
|
|
203
|
-
/>
|
|
204
|
-
<TouchableOpacity
|
|
205
|
-
onPress={sendMessage}
|
|
206
|
-
disabled={isGenerating || !input.trim()}
|
|
207
|
-
style={{
|
|
208
|
-
backgroundColor: isGenerating ? '#cccccc' : '#007AFF',
|
|
209
|
-
borderRadius: 20,
|
|
210
|
-
paddingHorizontal: 16,
|
|
211
|
-
paddingVertical: 10,
|
|
212
|
-
marginLeft: 8,
|
|
213
|
-
justifyContent: 'center',
|
|
214
|
-
}}
|
|
215
|
-
>
|
|
216
|
-
<Text style={{ color: '#ffffff', fontWeight: 'bold' }}>
|
|
217
|
-
{isGenerating ? '...' : 'Send'}
|
|
218
|
-
</Text>
|
|
219
|
-
</TouchableOpacity>
|
|
220
|
-
</View>
|
|
221
|
-
</View>
|
|
222
|
-
);
|
|
223
|
-
}
|
|
27
|
+
const params = { n_predict: 100, temperature: 0.7 };
|
|
28
|
+
const response = await lm.completion(messages, params);
|
|
224
29
|
```
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
### CactusLM
|
|
229
|
-
|
|
230
|
-
```typescript
|
|
30
|
+
## Embeddings
|
|
31
|
+
```typescript
|
|
231
32
|
import { CactusLM } from 'cactus-react-native';
|
|
232
33
|
|
|
233
34
|
const { lm, error } = await CactusLM.init({
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
n_gpu_layers: 99,
|
|
238
|
-
embedding: true,
|
|
35
|
+
model: '/path/to/model.gguf', // local model file inside the app sandbox
|
|
36
|
+
n_ctx: 2048,
|
|
37
|
+
embedding: true,
|
|
239
38
|
});
|
|
240
39
|
|
|
241
|
-
const
|
|
242
|
-
const
|
|
243
|
-
|
|
244
|
-
temperature: 0.7,
|
|
245
|
-
stop: ['</s>'],
|
|
246
|
-
});
|
|
247
|
-
|
|
248
|
-
const embedding = await lm.embedding('Your text here');
|
|
249
|
-
await lm.rewind();
|
|
250
|
-
await lm.release();
|
|
40
|
+
const text = 'Your text to embed';
|
|
41
|
+
const params = { normalize: true };
|
|
42
|
+
const result = await lm.embedding(text, params);
|
|
251
43
|
```
|
|
252
|
-
|
|
253
|
-
### CactusVLM
|
|
254
|
-
|
|
44
|
+
## Visual Language Models
|
|
255
45
|
```typescript
|
|
256
46
|
import { CactusVLM } from 'cactus-react-native';
|
|
257
47
|
|
|
258
48
|
const { vlm, error } = await CactusVLM.init({
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
n_ctx: 2048,
|
|
49
|
+
model: '/path/to/vision-model.gguf', // local model file inside the app sandbox
|
|
50
|
+
mmproj: '/path/to/mmproj.gguf', // local model file inside the app sandbox
|
|
262
51
|
});
|
|
263
52
|
|
|
264
53
|
const messages = [{ role: 'user', content: 'Describe this image' }];
|
|
265
|
-
const result = await vlm.completion(messages, {
|
|
266
|
-
images: ['/path/to/image.jpg'],
|
|
267
|
-
n_predict: 200,
|
|
268
|
-
temperature: 0.3,
|
|
269
|
-
});
|
|
270
|
-
|
|
271
|
-
await vlm.release();
|
|
272
|
-
```
|
|
273
|
-
|
|
274
|
-
### CactusTTS
|
|
275
|
-
|
|
276
|
-
```typescript
|
|
277
|
-
import { CactusTTS, initLlama } from 'cactus-react-native';
|
|
278
|
-
|
|
279
|
-
const context = await initLlama({
|
|
280
|
-
model: '/path/to/tts-model.gguf',
|
|
281
|
-
n_ctx: 1024,
|
|
282
|
-
});
|
|
283
54
|
|
|
284
|
-
const
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
'{"speaker_id": 0}'
|
|
289
|
-
);
|
|
290
|
-
|
|
291
|
-
await tts.release();
|
|
292
|
-
```
|
|
293
|
-
|
|
294
|
-
## Advanced Usage
|
|
295
|
-
|
|
296
|
-
### Model Manager
|
|
297
|
-
|
|
298
|
-
```typescript
|
|
299
|
-
class ModelManager {
|
|
300
|
-
private models = new Map<string, CactusLM | CactusVLM>();
|
|
301
|
-
|
|
302
|
-
async loadLM(name: string, modelPath: string): Promise<CactusLM> {
|
|
303
|
-
if (this.models.has(name)) {
|
|
304
|
-
return this.models.get(name) as CactusLM;
|
|
305
|
-
}
|
|
306
|
-
|
|
307
|
-
const { lm, error } = await CactusLM.init({
|
|
308
|
-
model: modelPath,
|
|
309
|
-
n_ctx: 2048,
|
|
310
|
-
});
|
|
311
|
-
|
|
312
|
-
if (error) throw error;
|
|
313
|
-
this.models.set(name, lm);
|
|
314
|
-
return lm;
|
|
315
|
-
}
|
|
316
|
-
|
|
317
|
-
async loadVLM(name: string, modelPath: string, mmprojPath: string): Promise<CactusVLM> {
|
|
318
|
-
if (this.models.has(name)) {
|
|
319
|
-
return this.models.get(name) as CactusVLM;
|
|
320
|
-
}
|
|
321
|
-
|
|
322
|
-
const { vlm, error } = await CactusVLM.init({
|
|
323
|
-
model: modelPath,
|
|
324
|
-
mmproj: mmprojPath,
|
|
325
|
-
});
|
|
326
|
-
|
|
327
|
-
if (error) throw error;
|
|
328
|
-
this.models.set(name, vlm);
|
|
329
|
-
return vlm;
|
|
330
|
-
}
|
|
331
|
-
|
|
332
|
-
async releaseModel(name: string): Promise<void> {
|
|
333
|
-
const model = this.models.get(name);
|
|
334
|
-
if (model) {
|
|
335
|
-
await model.release();
|
|
336
|
-
this.models.delete(name);
|
|
337
|
-
}
|
|
338
|
-
}
|
|
339
|
-
|
|
340
|
-
async releaseAll(): Promise<void> {
|
|
341
|
-
await Promise.all(
|
|
342
|
-
Array.from(this.models.values()).map(model => model.release())
|
|
343
|
-
);
|
|
344
|
-
this.models.clear();
|
|
345
|
-
}
|
|
346
|
-
}
|
|
347
|
-
|
|
348
|
-
const modelManager = new ModelManager();
|
|
349
|
-
```
|
|
350
|
-
|
|
351
|
-
### File Management Hook
|
|
352
|
-
|
|
353
|
-
```typescript
|
|
354
|
-
import { useState, useCallback } from 'react';
|
|
355
|
-
import RNFS from 'react-native-fs';
|
|
356
|
-
|
|
357
|
-
interface DownloadProgress {
|
|
358
|
-
progress: number;
|
|
359
|
-
isDownloading: boolean;
|
|
360
|
-
error: string | null;
|
|
361
|
-
}
|
|
362
|
-
|
|
363
|
-
export const useModelDownload = () => {
|
|
364
|
-
const [downloads, setDownloads] = useState<Map<string, DownloadProgress>>(new Map());
|
|
365
|
-
|
|
366
|
-
const downloadModel = useCallback(async (url: string, filename: string): Promise<string> => {
|
|
367
|
-
const path = `${RNFS.DocumentDirectoryPath}/${filename}`;
|
|
368
|
-
|
|
369
|
-
if (await RNFS.exists(path)) {
|
|
370
|
-
const stats = await RNFS.stat(path);
|
|
371
|
-
if (stats.size > 0) return path;
|
|
372
|
-
}
|
|
373
|
-
|
|
374
|
-
setDownloads(prev => new Map(prev.set(filename, {
|
|
375
|
-
progress: 0,
|
|
376
|
-
isDownloading: true,
|
|
377
|
-
error: null,
|
|
378
|
-
})));
|
|
379
|
-
|
|
380
|
-
try {
|
|
381
|
-
await RNFS.downloadFile({
|
|
382
|
-
fromUrl: url,
|
|
383
|
-
toFile: path,
|
|
384
|
-
progress: (res) => {
|
|
385
|
-
const progress = res.bytesWritten / res.contentLength;
|
|
386
|
-
setDownloads(prev => new Map(prev.set(filename, {
|
|
387
|
-
progress,
|
|
388
|
-
isDownloading: true,
|
|
389
|
-
error: null,
|
|
390
|
-
})));
|
|
391
|
-
},
|
|
392
|
-
}).promise;
|
|
393
|
-
|
|
394
|
-
setDownloads(prev => new Map(prev.set(filename, {
|
|
395
|
-
progress: 1,
|
|
396
|
-
isDownloading: false,
|
|
397
|
-
error: null,
|
|
398
|
-
})));
|
|
399
|
-
|
|
400
|
-
return path;
|
|
401
|
-
} catch (error) {
|
|
402
|
-
setDownloads(prev => new Map(prev.set(filename, {
|
|
403
|
-
progress: 0,
|
|
404
|
-
isDownloading: false,
|
|
405
|
-
error: error.message,
|
|
406
|
-
})));
|
|
407
|
-
throw error;
|
|
408
|
-
}
|
|
409
|
-
}, []);
|
|
410
|
-
|
|
411
|
-
return { downloadModel, downloads };
|
|
55
|
+
const params = {
|
|
56
|
+
images: ['/absolute/path/to/image.jpg'],
|
|
57
|
+
n_predict: 200,
|
|
58
|
+
temperature: 0.3,
|
|
412
59
|
};
|
|
413
|
-
```
|
|
414
|
-
|
|
415
|
-
### Vision Chat Component
|
|
416
|
-
|
|
417
|
-
```typescript
|
|
418
|
-
import React, { useState, useEffect } from 'react';
|
|
419
|
-
import { View, Text, TouchableOpacity, Image, Alert } from 'react-native';
|
|
420
|
-
import { launchImageLibrary } from 'react-native-image-picker';
|
|
421
|
-
import { CactusVLM } from 'cactus-react-native';
|
|
422
|
-
import RNFS from 'react-native-fs';
|
|
423
|
-
|
|
424
|
-
export default function VisionChat() {
|
|
425
|
-
const [vlm, setVLM] = useState<CactusVLM | null>(null);
|
|
426
|
-
const [imagePath, setImagePath] = useState<string | null>(null);
|
|
427
|
-
const [response, setResponse] = useState('');
|
|
428
|
-
const [isLoading, setIsLoading] = useState(true);
|
|
429
|
-
const [isAnalyzing, setIsAnalyzing] = useState(false);
|
|
430
|
-
|
|
431
|
-
useEffect(() => {
|
|
432
|
-
initializeVLM();
|
|
433
|
-
return () => {
|
|
434
|
-
vlm?.release();
|
|
435
|
-
};
|
|
436
|
-
}, []);
|
|
437
60
|
|
|
438
|
-
|
|
439
|
-
try {
|
|
440
|
-
const modelUrl = 'https://huggingface.co/Cactus-Compute/SmolVLM2-500m-Instruct-GGUF/resolve/main/SmolVLM2-500M-Video-Instruct-Q8_0.gguf';
|
|
441
|
-
const mmprojUrl = 'https://huggingface.co/Cactus-Compute/SmolVLM2-500m-Instruct-GGUF/resolve/main/mmproj-SmolVLM2-500M-Video-Instruct-Q8_0.gguf';
|
|
442
|
-
|
|
443
|
-
const [modelPath, mmprojPath] = await Promise.all([
|
|
444
|
-
downloadFile(modelUrl, 'smolvlm-model.gguf'),
|
|
445
|
-
downloadFile(mmprojUrl, 'smolvlm-mmproj.gguf'),
|
|
446
|
-
]);
|
|
447
|
-
|
|
448
|
-
const { vlm: model, error } = await CactusVLM.init({
|
|
449
|
-
model: modelPath,
|
|
450
|
-
mmproj: mmprojPath,
|
|
451
|
-
n_ctx: 2048,
|
|
452
|
-
});
|
|
453
|
-
|
|
454
|
-
if (error) throw error;
|
|
455
|
-
setVLM(model);
|
|
456
|
-
} catch (error) {
|
|
457
|
-
console.error('Failed to initialize VLM:', error);
|
|
458
|
-
Alert.alert('Error', 'Failed to initialize vision model');
|
|
459
|
-
} finally {
|
|
460
|
-
setIsLoading(false);
|
|
461
|
-
}
|
|
462
|
-
};
|
|
463
|
-
|
|
464
|
-
const downloadFile = async (url: string, filename: string): Promise<string> => {
|
|
465
|
-
const path = `${RNFS.DocumentDirectoryPath}/${filename}`;
|
|
466
|
-
|
|
467
|
-
if (await RNFS.exists(path)) return path;
|
|
468
|
-
|
|
469
|
-
await RNFS.downloadFile({ fromUrl: url, toFile: path }).promise;
|
|
470
|
-
return path;
|
|
471
|
-
};
|
|
472
|
-
|
|
473
|
-
const pickImage = () => {
|
|
474
|
-
launchImageLibrary(
|
|
475
|
-
{
|
|
476
|
-
mediaType: 'photo',
|
|
477
|
-
quality: 0.8,
|
|
478
|
-
includeBase64: false,
|
|
479
|
-
},
|
|
480
|
-
(response) => {
|
|
481
|
-
if (response.assets && response.assets[0]) {
|
|
482
|
-
setImagePath(response.assets[0].uri!);
|
|
483
|
-
setResponse('');
|
|
484
|
-
}
|
|
485
|
-
}
|
|
486
|
-
);
|
|
487
|
-
};
|
|
488
|
-
|
|
489
|
-
const analyzeImage = async () => {
|
|
490
|
-
if (!vlm || !imagePath) return;
|
|
491
|
-
|
|
492
|
-
setIsAnalyzing(true);
|
|
493
|
-
try {
|
|
494
|
-
const messages = [{ role: 'user', content: 'Describe this image in detail' }];
|
|
495
|
-
|
|
496
|
-
let analysisResponse = '';
|
|
497
|
-
const result = await vlm.completion(messages, {
|
|
498
|
-
images: [imagePath],
|
|
499
|
-
n_predict: 300,
|
|
500
|
-
temperature: 0.3,
|
|
501
|
-
}, (token) => {
|
|
502
|
-
analysisResponse += token.token;
|
|
503
|
-
setResponse(analysisResponse);
|
|
504
|
-
});
|
|
505
|
-
|
|
506
|
-
setResponse(analysisResponse || result.text);
|
|
507
|
-
} catch (error) {
|
|
508
|
-
console.error('Analysis failed:', error);
|
|
509
|
-
Alert.alert('Error', 'Failed to analyze image');
|
|
510
|
-
} finally {
|
|
511
|
-
setIsAnalyzing(false);
|
|
512
|
-
}
|
|
513
|
-
};
|
|
514
|
-
|
|
515
|
-
if (isLoading) {
|
|
516
|
-
return (
|
|
517
|
-
<View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}>
|
|
518
|
-
<Text>Loading vision model...</Text>
|
|
519
|
-
</View>
|
|
520
|
-
);
|
|
521
|
-
}
|
|
522
|
-
|
|
523
|
-
return (
|
|
524
|
-
<View style={{ flex: 1, padding: 16 }}>
|
|
525
|
-
<Text style={{ fontSize: 24, fontWeight: 'bold', marginBottom: 20 }}>
|
|
526
|
-
Vision Chat
|
|
527
|
-
</Text>
|
|
528
|
-
|
|
529
|
-
{imagePath && (
|
|
530
|
-
<Image
|
|
531
|
-
source={{ uri: imagePath }}
|
|
532
|
-
style={{
|
|
533
|
-
width: '100%',
|
|
534
|
-
height: 200,
|
|
535
|
-
borderRadius: 8,
|
|
536
|
-
marginBottom: 16,
|
|
537
|
-
}}
|
|
538
|
-
resizeMode="contain"
|
|
539
|
-
/>
|
|
540
|
-
)}
|
|
541
|
-
|
|
542
|
-
<View style={{ flexDirection: 'row', marginBottom: 16 }}>
|
|
543
|
-
<TouchableOpacity
|
|
544
|
-
onPress={pickImage}
|
|
545
|
-
style={{
|
|
546
|
-
backgroundColor: '#007AFF',
|
|
547
|
-
padding: 12,
|
|
548
|
-
borderRadius: 8,
|
|
549
|
-
marginRight: 8,
|
|
550
|
-
flex: 1,
|
|
551
|
-
}}
|
|
552
|
-
>
|
|
553
|
-
<Text style={{ color: 'white', textAlign: 'center', fontWeight: 'bold' }}>
|
|
554
|
-
Pick Image
|
|
555
|
-
</Text>
|
|
556
|
-
</TouchableOpacity>
|
|
557
|
-
|
|
558
|
-
<TouchableOpacity
|
|
559
|
-
onPress={analyzeImage}
|
|
560
|
-
disabled={!imagePath || isAnalyzing}
|
|
561
|
-
style={{
|
|
562
|
-
backgroundColor: !imagePath || isAnalyzing ? '#cccccc' : '#34C759',
|
|
563
|
-
padding: 12,
|
|
564
|
-
borderRadius: 8,
|
|
565
|
-
flex: 1,
|
|
566
|
-
}}
|
|
567
|
-
>
|
|
568
|
-
<Text style={{ color: 'white', textAlign: 'center', fontWeight: 'bold' }}>
|
|
569
|
-
{isAnalyzing ? 'Analyzing...' : 'Analyze'}
|
|
570
|
-
</Text>
|
|
571
|
-
</TouchableOpacity>
|
|
572
|
-
</View>
|
|
573
|
-
|
|
574
|
-
<View style={{
|
|
575
|
-
flex: 1,
|
|
576
|
-
backgroundColor: '#f8f8f8',
|
|
577
|
-
borderRadius: 8,
|
|
578
|
-
padding: 16,
|
|
579
|
-
}}>
|
|
580
|
-
<Text style={{ fontSize: 16, lineHeight: 24 }}>
|
|
581
|
-
{response || 'Select an image and tap Analyze to get started'}
|
|
582
|
-
</Text>
|
|
583
|
-
</View>
|
|
584
|
-
</View>
|
|
585
|
-
);
|
|
586
|
-
}
|
|
61
|
+
const response = await vlm.completion(messages, params);
|
|
587
62
|
```
|
|
588
|
-
|
|
589
|
-
### Cloud Fallback
|
|
590
|
-
|
|
591
|
-
```typescript
|
|
592
|
-
const { vlm } = await CactusVLM.init({
|
|
593
|
-
model: '/path/to/model.gguf',
|
|
594
|
-
mmproj: '/path/to/mmproj.gguf',
|
|
595
|
-
}, undefined, 'your_cactus_token');
|
|
596
|
-
|
|
597
|
-
const result = await vlm.completion(messages, {
|
|
598
|
-
images: ['/path/to/image.jpg'],
|
|
599
|
-
mode: 'localfirst', // ("remotefirst", "local", "remote")
|
|
600
|
-
});
|
|
601
|
-
```
|
|
602
|
-
|
|
603
|
-
### Embeddings & Similarity
|
|
604
|
-
|
|
63
|
+
## Cloud Fallback
|
|
605
64
|
```typescript
|
|
606
65
|
const { lm } = await CactusLM.init({
|
|
607
66
|
model: '/path/to/model.gguf',
|
|
608
|
-
|
|
609
|
-
});
|
|
610
|
-
|
|
611
|
-
const embedding1 = await lm.embedding('machine learning');
|
|
612
|
-
const embedding2 = await lm.embedding('artificial intelligence');
|
|
613
|
-
|
|
614
|
-
function cosineSimilarity(a: number[], b: number[]): number {
|
|
615
|
-
const dotProduct = a.reduce((sum, ai, i) => sum + ai * b[i], 0);
|
|
616
|
-
const magnitudeA = Math.sqrt(a.reduce((sum, ai) => sum + ai * ai, 0));
|
|
617
|
-
const magnitudeB = Math.sqrt(b.reduce((sum, bi) => sum + bi * bi, 0));
|
|
618
|
-
return dotProduct / (magnitudeA * magnitudeB);
|
|
619
|
-
}
|
|
620
|
-
|
|
621
|
-
const similarity = cosineSimilarity(embedding1.embedding, embedding2.embedding);
|
|
622
|
-
console.log('Similarity:', similarity);
|
|
623
|
-
```
|
|
624
|
-
|
|
625
|
-
## Error Handling & Performance
|
|
626
|
-
|
|
627
|
-
### Production Error Handling
|
|
628
|
-
|
|
629
|
-
```typescript
|
|
630
|
-
async function safeModelInit(modelPath: string): Promise<CactusLM> {
|
|
631
|
-
const configs = [
|
|
632
|
-
{ model: modelPath, n_ctx: 4096, n_gpu_layers: 99 },
|
|
633
|
-
{ model: modelPath, n_ctx: 2048, n_gpu_layers: 99 },
|
|
634
|
-
{ model: modelPath, n_ctx: 2048, n_gpu_layers: 0 },
|
|
635
|
-
{ model: modelPath, n_ctx: 1024, n_gpu_layers: 0 },
|
|
636
|
-
];
|
|
637
|
-
|
|
638
|
-
for (const config of configs) {
|
|
639
|
-
try {
|
|
640
|
-
const { lm, error } = await CactusLM.init(config);
|
|
641
|
-
if (error) throw error;
|
|
642
|
-
return lm;
|
|
643
|
-
} catch (error) {
|
|
644
|
-
console.warn('Config failed:', config, error.message);
|
|
645
|
-
if (configs.indexOf(config) === configs.length - 1) {
|
|
646
|
-
throw new Error(`All configurations failed. Last error: ${error.message}`);
|
|
647
|
-
}
|
|
648
|
-
}
|
|
649
|
-
}
|
|
650
|
-
|
|
651
|
-
throw new Error('Model initialization failed');
|
|
652
|
-
}
|
|
653
|
-
|
|
654
|
-
async function safeCompletion(lm: CactusLM, messages: any[], retries = 3): Promise<any> {
|
|
655
|
-
for (let i = 0; i < retries; i++) {
|
|
656
|
-
try {
|
|
657
|
-
return await lm.completion(messages, { n_predict: 200 });
|
|
658
|
-
} catch (error) {
|
|
659
|
-
if (error.message.includes('Context is busy') && i < retries - 1) {
|
|
660
|
-
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
661
|
-
continue;
|
|
662
|
-
}
|
|
663
|
-
throw error;
|
|
664
|
-
}
|
|
665
|
-
}
|
|
666
|
-
}
|
|
667
|
-
```
|
|
668
|
-
|
|
669
|
-
### Memory Management
|
|
670
|
-
|
|
671
|
-
```typescript
|
|
672
|
-
import { AppState, AppStateStatus } from 'react-native';
|
|
673
|
-
|
|
674
|
-
class AppModelManager {
|
|
675
|
-
private modelManager = new ModelManager();
|
|
676
|
-
|
|
677
|
-
constructor() {
|
|
678
|
-
AppState.addEventListener('change', this.handleAppStateChange);
|
|
679
|
-
}
|
|
680
|
-
|
|
681
|
-
private handleAppStateChange = (nextAppState: AppStateStatus) => {
|
|
682
|
-
if (nextAppState === 'background') {
|
|
683
|
-
// Release non-essential models when app goes to background
|
|
684
|
-
this.modelManager.releaseAll();
|
|
685
|
-
}
|
|
686
|
-
};
|
|
687
|
-
|
|
688
|
-
async getModel(name: string, modelPath: string): Promise<CactusLM> {
|
|
689
|
-
try {
|
|
690
|
-
return await this.modelManager.loadLM(name, modelPath);
|
|
691
|
-
} catch (error) {
|
|
692
|
-
// Handle low memory by releasing other models
|
|
693
|
-
await this.modelManager.releaseAll();
|
|
694
|
-
return await this.modelManager.loadLM(name, modelPath);
|
|
695
|
-
}
|
|
696
|
-
}
|
|
697
|
-
}
|
|
698
|
-
```
|
|
699
|
-
|
|
700
|
-
### Performance Optimization
|
|
67
|
+
n_ctx: 2048,
|
|
68
|
+
}, undefined, 'your_cactus_token');
|
|
701
69
|
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
const getOptimalConfig = () => {
|
|
705
|
-
const { OS } = Platform;
|
|
706
|
-
const isHighEndDevice = true; // Implement device detection logic
|
|
707
|
-
|
|
708
|
-
return {
|
|
709
|
-
n_ctx: isHighEndDevice ? 4096 : 2048,
|
|
710
|
-
n_gpu_layers: OS === 'ios' ? 99 : 0, // iOS generally has better GPU support
|
|
711
|
-
n_threads: isHighEndDevice ? 6 : 4,
|
|
712
|
-
n_batch: isHighEndDevice ? 512 : 256,
|
|
713
|
-
};
|
|
714
|
-
};
|
|
70
|
+
// Try local first, fallback to cloud if local fails (its blazing fast)
|
|
71
|
+
const embedding = await lm.embedding('text', undefined, 'localfirst');
|
|
715
72
|
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
});
|
|
73
|
+
// local (default): strictly only run on-device
|
|
74
|
+
// localfirst: fallback to cloud if device fails
|
|
75
|
+
// remotefirst: primarily remote, run local if API fails
|
|
76
|
+
// remote: strictly run on cloud
|
|
721
77
|
```
|
|
722
78
|
|
|
723
|
-
##
|
|
724
|
-
|
|
725
|
-
The `CactusAgent` class extends `CactusLM` with built-in tool calling capabilities:
|
|
726
|
-
|
|
79
|
+
## Agents
|
|
727
80
|
```typescript
|
|
728
81
|
import { CactusAgent } from 'cactus-react-native';
|
|
729
82
|
|
|
83
|
+
// we recommend Qwen 3 family, 0.6B is great
|
|
730
84
|
const { agent, error } = await CactusAgent.init({
|
|
731
|
-
|
|
732
|
-
|
|
85
|
+
model: '/path/to/model.gguf',
|
|
86
|
+
n_ctx: 2048,
|
|
733
87
|
});
|
|
734
88
|
|
|
735
89
|
const weatherTool = agent.addTool(
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
90
|
+
(location: string) => `Weather in ${location}: 72°F, sunny`,
|
|
91
|
+
'Get current weather for a location',
|
|
92
|
+
{
|
|
93
|
+
location: { type: 'string', description: 'City name', required: true }
|
|
94
|
+
}
|
|
741
95
|
);
|
|
742
96
|
|
|
743
97
|
const messages = [{ role: 'user', content: 'What\'s the weather in NYC?' }];
|
|
744
|
-
const result = await agent.completionWithTools(messages, {
|
|
98
|
+
const result = await agent.completionWithTools(messages, {
|
|
745
99
|
n_predict: 200,
|
|
746
100
|
temperature: 0.7,
|
|
747
101
|
});
|
|
748
102
|
|
|
749
103
|
await agent.release();
|
|
750
|
-
```
|
|
751
|
-
|
|
752
|
-
### Custom Tools
|
|
753
|
-
|
|
754
|
-
```typescript
|
|
755
|
-
// Math calculator tool
|
|
756
|
-
const calculator = agent.addTool(
|
|
757
|
-
(expression: string) => {
|
|
758
|
-
try {
|
|
759
|
-
return `Result: ${eval(expression)}`;
|
|
760
|
-
} catch (e) {
|
|
761
|
-
return 'Invalid expression';
|
|
762
|
-
}
|
|
763
|
-
},
|
|
764
|
-
'Evaluate mathematical expressions',
|
|
765
|
-
{
|
|
766
|
-
expression: { type: 'string', description: 'Math expression to evaluate', required: true }
|
|
767
|
-
}
|
|
768
|
-
);
|
|
769
|
-
```
|
|
770
|
-
|
|
771
|
-
## API Reference
|
|
772
|
-
|
|
773
|
-
### CactusLM
|
|
774
|
-
|
|
775
|
-
**init(params, onProgress?, cactusToken?)**
|
|
776
|
-
- `model: string` - Path to GGUF model file
|
|
777
|
-
- `n_ctx?: number` - Context size (default: 2048)
|
|
778
|
-
- `n_threads?: number` - CPU threads (default: 4)
|
|
779
|
-
- `n_gpu_layers?: number` - GPU layers (default: 99)
|
|
780
|
-
- `embedding?: boolean` - Enable embeddings (default: false)
|
|
781
|
-
- `n_batch?: number` - Batch size (default: 512)
|
|
782
|
-
|
|
783
|
-
**completion(messages, params?, callback?)**
|
|
784
|
-
- `messages: Array<{role: string, content: string}>` - Chat messages
|
|
785
|
-
- `n_predict?: number` - Max tokens (default: -1)
|
|
786
|
-
- `temperature?: number` - Randomness 0.0-2.0 (default: 0.8)
|
|
787
|
-
- `top_p?: number` - Nucleus sampling (default: 0.95)
|
|
788
|
-
- `top_k?: number` - Top-k sampling (default: 40)
|
|
789
|
-
- `stop?: string[]` - Stop sequences
|
|
790
|
-
- `callback?: (token) => void` - Streaming callback
|
|
791
|
-
|
|
792
|
-
**embedding(text, params?, mode?)**
|
|
793
|
-
- `text: string` - Text to embed
|
|
794
|
-
- `mode?: string` - 'local' | 'localfirst' | 'remotefirst' | 'remote'
|
|
795
|
-
|
|
796
|
-
### CactusVLM
|
|
797
|
-
|
|
798
|
-
**init(params, onProgress?, cactusToken?)**
|
|
799
|
-
- All CactusLM params plus:
|
|
800
|
-
- `mmproj: string` - Path to multimodal projector
|
|
801
|
-
|
|
802
|
-
**completion(messages, params?, callback?)**
|
|
803
|
-
- All CactusLM completion params plus:
|
|
804
|
-
- `images?: string[]` - Array of image paths
|
|
805
|
-
- `mode?: string` - Cloud fallback mode
|
|
806
|
-
|
|
807
|
-
### Types
|
|
808
|
-
|
|
809
|
-
```typescript
|
|
810
|
-
interface CactusOAICompatibleMessage {
|
|
811
|
-
role: 'system' | 'user' | 'assistant';
|
|
812
|
-
content: string;
|
|
813
|
-
}
|
|
814
|
-
|
|
815
|
-
interface NativeCompletionResult {
|
|
816
|
-
text: string;
|
|
817
|
-
tokens_predicted: number;
|
|
818
|
-
tokens_evaluated: number;
|
|
819
|
-
timings: {
|
|
820
|
-
predicted_per_second: number;
|
|
821
|
-
prompt_per_second: number;
|
|
822
|
-
};
|
|
823
|
-
}
|
|
824
|
-
|
|
825
|
-
interface NativeEmbeddingResult {
|
|
826
|
-
embedding: number[];
|
|
827
|
-
}
|
|
828
|
-
```
|
|
104
|
+
```
|