cactus-react-native 0.2.9 → 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 +49 -782
- 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/agent.js +3 -0
- package/lib/commonjs/agent.js.map +1 -1
- package/lib/commonjs/index.js.map +1 -1
- package/lib/commonjs/lm.js +3 -0
- package/lib/commonjs/lm.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/commonjs/vlm.js +3 -0
- package/lib/commonjs/vlm.js.map +1 -1
- package/lib/module/agent.js +3 -0
- package/lib/module/agent.js.map +1 -1
- package/lib/module/index.js.map +1 -1
- package/lib/module/lm.js +3 -0
- package/lib/module/lm.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/module/vlm.js +3 -0
- package/lib/module/vlm.js.map +1 -1
- package/lib/typescript/agent.d.ts +1 -0
- package/lib/typescript/agent.d.ts.map +1 -1
- package/lib/typescript/index.d.ts +1 -1
- package/lib/typescript/index.d.ts.map +1 -1
- package/lib/typescript/lm.d.ts +1 -0
- package/lib/typescript/lm.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/lib/typescript/vlm.d.ts +1 -0
- package/lib/typescript/vlm.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/agent.ts +3 -0
- package/src/index.ts +1 -0
- package/src/lm.ts +3 -0
- package/src/tts.ts +209 -15
- package/src/vlm.ts +4 -0
package/README.md
CHANGED
|
@@ -1,837 +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
|
-
|
|
284
|
-
const tts = await CactusTTS.init(context, '/path/to/vocoder.gguf');
|
|
285
|
-
|
|
286
|
-
const audio = await tts.generate(
|
|
287
|
-
'Hello, this is text-to-speech',
|
|
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
54
|
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
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
|
-
|
|
438
|
-
const initializeVLM = async () => {
|
|
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
60
|
|
|
448
|
-
|
|
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
|
-
|
|
63
|
+
## Cloud Fallback
|
|
591
64
|
```typescript
|
|
592
65
|
const { lm } = await CactusLM.init({
|
|
593
66
|
model: '/path/to/model.gguf',
|
|
594
67
|
n_ctx: 2048,
|
|
595
68
|
}, undefined, 'your_cactus_token');
|
|
596
69
|
|
|
597
|
-
// Try local first, fallback to cloud if local fails
|
|
70
|
+
// Try local first, fallback to cloud if local fails (its blazing fast)
|
|
598
71
|
const embedding = await lm.embedding('text', undefined, 'localfirst');
|
|
599
72
|
|
|
600
|
-
//
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
}, undefined, 'your_cactus_token');
|
|
605
|
-
|
|
606
|
-
const result = await vlm.completion(messages, {
|
|
607
|
-
images: ['/path/to/image.jpg'],
|
|
608
|
-
mode: 'localfirst',
|
|
609
|
-
});
|
|
610
|
-
```
|
|
611
|
-
|
|
612
|
-
### Embeddings & Similarity
|
|
613
|
-
|
|
614
|
-
```typescript
|
|
615
|
-
const { lm } = await CactusLM.init({
|
|
616
|
-
model: '/path/to/model.gguf',
|
|
617
|
-
embedding: true,
|
|
618
|
-
});
|
|
619
|
-
|
|
620
|
-
const embedding1 = await lm.embedding('machine learning');
|
|
621
|
-
const embedding2 = await lm.embedding('artificial intelligence');
|
|
622
|
-
|
|
623
|
-
function cosineSimilarity(a: number[], b: number[]): number {
|
|
624
|
-
const dotProduct = a.reduce((sum, ai, i) => sum + ai * b[i], 0);
|
|
625
|
-
const magnitudeA = Math.sqrt(a.reduce((sum, ai) => sum + ai * ai, 0));
|
|
626
|
-
const magnitudeB = Math.sqrt(b.reduce((sum, bi) => sum + bi * bi, 0));
|
|
627
|
-
return dotProduct / (magnitudeA * magnitudeB);
|
|
628
|
-
}
|
|
629
|
-
|
|
630
|
-
const similarity = cosineSimilarity(embedding1.embedding, embedding2.embedding);
|
|
631
|
-
console.log('Similarity:', similarity);
|
|
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
|
|
632
77
|
```
|
|
633
78
|
|
|
634
|
-
##
|
|
635
|
-
|
|
636
|
-
### Production Error Handling
|
|
637
|
-
|
|
638
|
-
```typescript
|
|
639
|
-
async function safeModelInit(modelPath: string): Promise<CactusLM> {
|
|
640
|
-
const configs = [
|
|
641
|
-
{ model: modelPath, n_ctx: 4096, n_gpu_layers: 99 },
|
|
642
|
-
{ model: modelPath, n_ctx: 2048, n_gpu_layers: 99 },
|
|
643
|
-
{ model: modelPath, n_ctx: 2048, n_gpu_layers: 0 },
|
|
644
|
-
{ model: modelPath, n_ctx: 1024, n_gpu_layers: 0 },
|
|
645
|
-
];
|
|
646
|
-
|
|
647
|
-
for (const config of configs) {
|
|
648
|
-
try {
|
|
649
|
-
const { lm, error } = await CactusLM.init(config);
|
|
650
|
-
if (error) throw error;
|
|
651
|
-
return lm;
|
|
652
|
-
} catch (error) {
|
|
653
|
-
console.warn('Config failed:', config, error.message);
|
|
654
|
-
if (configs.indexOf(config) === configs.length - 1) {
|
|
655
|
-
throw new Error(`All configurations failed. Last error: ${error.message}`);
|
|
656
|
-
}
|
|
657
|
-
}
|
|
658
|
-
}
|
|
659
|
-
|
|
660
|
-
throw new Error('Model initialization failed');
|
|
661
|
-
}
|
|
662
|
-
|
|
663
|
-
async function safeCompletion(lm: CactusLM, messages: any[], retries = 3): Promise<any> {
|
|
664
|
-
for (let i = 0; i < retries; i++) {
|
|
665
|
-
try {
|
|
666
|
-
return await lm.completion(messages, { n_predict: 200 });
|
|
667
|
-
} catch (error) {
|
|
668
|
-
if (error.message.includes('Context is busy') && i < retries - 1) {
|
|
669
|
-
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
670
|
-
continue;
|
|
671
|
-
}
|
|
672
|
-
throw error;
|
|
673
|
-
}
|
|
674
|
-
}
|
|
675
|
-
}
|
|
676
|
-
```
|
|
677
|
-
|
|
678
|
-
### Memory Management
|
|
679
|
-
|
|
680
|
-
```typescript
|
|
681
|
-
import { AppState, AppStateStatus } from 'react-native';
|
|
682
|
-
|
|
683
|
-
class AppModelManager {
|
|
684
|
-
private modelManager = new ModelManager();
|
|
685
|
-
|
|
686
|
-
constructor() {
|
|
687
|
-
AppState.addEventListener('change', this.handleAppStateChange);
|
|
688
|
-
}
|
|
689
|
-
|
|
690
|
-
private handleAppStateChange = (nextAppState: AppStateStatus) => {
|
|
691
|
-
if (nextAppState === 'background') {
|
|
692
|
-
// Release non-essential models when app goes to background
|
|
693
|
-
this.modelManager.releaseAll();
|
|
694
|
-
}
|
|
695
|
-
};
|
|
696
|
-
|
|
697
|
-
async getModel(name: string, modelPath: string): Promise<CactusLM> {
|
|
698
|
-
try {
|
|
699
|
-
return await this.modelManager.loadLM(name, modelPath);
|
|
700
|
-
} catch (error) {
|
|
701
|
-
// Handle low memory by releasing other models
|
|
702
|
-
await this.modelManager.releaseAll();
|
|
703
|
-
return await this.modelManager.loadLM(name, modelPath);
|
|
704
|
-
}
|
|
705
|
-
}
|
|
706
|
-
}
|
|
707
|
-
```
|
|
708
|
-
|
|
709
|
-
### Performance Optimization
|
|
710
|
-
|
|
711
|
-
```typescript
|
|
712
|
-
// Optimize for device capabilities
|
|
713
|
-
const getOptimalConfig = () => {
|
|
714
|
-
const { OS } = Platform;
|
|
715
|
-
const isHighEndDevice = true; // Implement device detection logic
|
|
716
|
-
|
|
717
|
-
return {
|
|
718
|
-
n_ctx: isHighEndDevice ? 4096 : 2048,
|
|
719
|
-
n_gpu_layers: OS === 'ios' ? 99 : 0, // iOS generally has better GPU support
|
|
720
|
-
n_threads: isHighEndDevice ? 6 : 4,
|
|
721
|
-
n_batch: isHighEndDevice ? 512 : 256,
|
|
722
|
-
};
|
|
723
|
-
};
|
|
724
|
-
|
|
725
|
-
const config = getOptimalConfig();
|
|
726
|
-
const { lm } = await CactusLM.init({
|
|
727
|
-
model: modelPath,
|
|
728
|
-
...config,
|
|
729
|
-
});
|
|
730
|
-
```
|
|
731
|
-
|
|
732
|
-
## Tool Calling with CactusAgent
|
|
733
|
-
|
|
734
|
-
The `CactusAgent` class extends `CactusLM` with built-in tool calling capabilities:
|
|
735
|
-
|
|
79
|
+
## Agents
|
|
736
80
|
```typescript
|
|
737
81
|
import { CactusAgent } from 'cactus-react-native';
|
|
738
82
|
|
|
83
|
+
// we recommend Qwen 3 family, 0.6B is great
|
|
739
84
|
const { agent, error } = await CactusAgent.init({
|
|
740
|
-
|
|
741
|
-
|
|
85
|
+
model: '/path/to/model.gguf',
|
|
86
|
+
n_ctx: 2048,
|
|
742
87
|
});
|
|
743
88
|
|
|
744
89
|
const weatherTool = agent.addTool(
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
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
|
+
}
|
|
750
95
|
);
|
|
751
96
|
|
|
752
97
|
const messages = [{ role: 'user', content: 'What\'s the weather in NYC?' }];
|
|
753
|
-
const result = await agent.completionWithTools(messages, {
|
|
98
|
+
const result = await agent.completionWithTools(messages, {
|
|
754
99
|
n_predict: 200,
|
|
755
100
|
temperature: 0.7,
|
|
756
101
|
});
|
|
757
102
|
|
|
758
103
|
await agent.release();
|
|
759
|
-
```
|
|
760
|
-
|
|
761
|
-
### Custom Tools
|
|
762
|
-
|
|
763
|
-
```typescript
|
|
764
|
-
// Math calculator tool
|
|
765
|
-
const calculator = agent.addTool(
|
|
766
|
-
(expression: string) => {
|
|
767
|
-
try {
|
|
768
|
-
return `Result: ${eval(expression)}`;
|
|
769
|
-
} catch (e) {
|
|
770
|
-
return 'Invalid expression';
|
|
771
|
-
}
|
|
772
|
-
},
|
|
773
|
-
'Evaluate mathematical expressions',
|
|
774
|
-
{
|
|
775
|
-
expression: { type: 'string', description: 'Math expression to evaluate', required: true }
|
|
776
|
-
}
|
|
777
|
-
);
|
|
778
|
-
```
|
|
779
|
-
|
|
780
|
-
## API Reference
|
|
781
|
-
|
|
782
|
-
### CactusLM
|
|
783
|
-
|
|
784
|
-
**init(params, onProgress?, cactusToken?)**
|
|
785
|
-
- `model: string` - Path to GGUF model file
|
|
786
|
-
- `n_ctx?: number` - Context size (default: 2048)
|
|
787
|
-
- `n_threads?: number` - CPU threads (default: 4)
|
|
788
|
-
- `n_gpu_layers?: number` - GPU layers (default: 99)
|
|
789
|
-
- `embedding?: boolean` - Enable embeddings (default: false)
|
|
790
|
-
- `n_batch?: number` - Batch size (default: 512)
|
|
791
|
-
|
|
792
|
-
**completion(messages, params?, callback?)**
|
|
793
|
-
- `messages: Array<{role: string, content: string}>` - Chat messages
|
|
794
|
-
- `n_predict?: number` - Max tokens (default: -1)
|
|
795
|
-
- `temperature?: number` - Randomness 0.0-2.0 (default: 0.8)
|
|
796
|
-
- `top_p?: number` - Nucleus sampling (default: 0.95)
|
|
797
|
-
- `top_k?: number` - Top-k sampling (default: 40)
|
|
798
|
-
- `stop?: string[]` - Stop sequences
|
|
799
|
-
- `callback?: (token) => void` - Streaming callback
|
|
800
|
-
|
|
801
|
-
**embedding(text, params?, mode?)**
|
|
802
|
-
- `text: string` - Text to embed
|
|
803
|
-
- `mode?: string` - 'local' | 'localfirst' | 'remotefirst' | 'remote'
|
|
804
|
-
|
|
805
|
-
### CactusVLM
|
|
806
|
-
|
|
807
|
-
**init(params, onProgress?, cactusToken?)**
|
|
808
|
-
- All CactusLM params plus:
|
|
809
|
-
- `mmproj: string` - Path to multimodal projector
|
|
810
|
-
|
|
811
|
-
**completion(messages, params?, callback?)**
|
|
812
|
-
- All CactusLM completion params plus:
|
|
813
|
-
- `images?: string[]` - Array of image paths
|
|
814
|
-
- `mode?: string` - Cloud fallback mode
|
|
815
|
-
|
|
816
|
-
### Types
|
|
817
|
-
|
|
818
|
-
```typescript
|
|
819
|
-
interface CactusOAICompatibleMessage {
|
|
820
|
-
role: 'system' | 'user' | 'assistant';
|
|
821
|
-
content: string;
|
|
822
|
-
}
|
|
823
|
-
|
|
824
|
-
interface NativeCompletionResult {
|
|
825
|
-
text: string;
|
|
826
|
-
tokens_predicted: number;
|
|
827
|
-
tokens_evaluated: number;
|
|
828
|
-
timings: {
|
|
829
|
-
predicted_per_second: number;
|
|
830
|
-
prompt_per_second: number;
|
|
831
|
-
};
|
|
832
|
-
}
|
|
833
|
-
|
|
834
|
-
interface NativeEmbeddingResult {
|
|
835
|
-
embedding: number[];
|
|
836
|
-
}
|
|
837
|
-
```
|
|
104
|
+
```
|