Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 61 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -1018,6 +1018,10 @@ Releases all resources associated with the model. Automatically calls `stop()` f

Returns available models.

**`getModelName(): string`**

Returns the model slug or path the instance was created with.

### useCactusLM Hook

The `useCactusLM` hook manages a `CactusLM` instance with reactive state. When model parameters (`model`, `corpusDir`, `cacheIndex`, `options`) change, the hook creates a new instance and resets all state. The hook automatically cleans up resources when the component unmounts.
Expand Down Expand Up @@ -1111,6 +1115,15 @@ Feeds audio samples into the streaming session and returns the current transcrip

Stops the streaming session and returns the final confirmed transcription text. Throws an error if no session is active.

**`detectLanguage(params: CactusSTTDetectLanguageParams): Promise<CactusSTTDetectLanguageResult>`**

Detects the spoken language in the given audio. Automatically calls `init()` if not already initialized. Throws an error if a generation is already in progress.

**Parameters:**
- `audio` - Path to the audio file or raw PCM samples as a byte array.
- `options`:
- `useVad` - Whether to apply VAD before detection (default: `true`).

**`audioEmbed(params: CactusSTTAudioEmbedParams): Promise<CactusSTTAudioEmbedResult>`**

Generates embeddings for the given audio file. Automatically calls `init()` if not already initialized. Throws an error if a generation is already in progress.
Expand All @@ -1134,6 +1147,10 @@ Releases all resources associated with the model. Stops any active streaming ses

Returns available speech-to-text models.

**`getModelName(): string`**

Returns the model slug or path the instance was created with.

### useCactusSTT Hook

The `useCactusSTT` hook manages a `CactusSTT` instance with reactive state. When model parameters (`model`, `options`) change, the hook creates a new instance and resets all state. The hook automatically cleans up resources when the component unmounts.
Expand Down Expand Up @@ -1216,6 +1233,10 @@ Releases all resources associated with the model. Safe to call even if the model

Returns available VAD models.

**`getModelName(): string`**

Returns the model slug or path the instance was created with.

### useCactusVAD Hook

The `useCactusVAD` hook manages a `CactusVAD` instance with reactive state. When model parameters (`model`, `options`) change, the hook creates a new instance and resets all state. The hook automatically cleans up resources when the component unmounts.
Expand Down Expand Up @@ -1314,6 +1335,20 @@ The `useCactusIndex` hook manages a `CactusIndex` instance with reactive state.
- `compact(): Promise<void>` - Optimizes the index. Sets `isProcessing` to `true` during operation.
- `destroy(): Promise<void>` - Releases all resources. Automatically called when the component unmounts.

### getRegistry

**`getRegistry(): Promise<{ [key: string]: CactusModel }>`**

Returns all available models from HuggingFace, keyed by model slug. Result is cached across calls.

```typescript
import { getRegistry } from 'cactus-react-native';

const registry = await getRegistry();
const model = registry['qwen3-0.6b'];
console.log(model.quantization.int4.url);
```

## Type Definitions

### CactusLMParams
Expand Down Expand Up @@ -1647,6 +1682,32 @@ interface CactusSTTStreamTranscribeStopResult {
}
```

### CactusSTTDetectLanguageOptions

```typescript
interface CactusSTTDetectLanguageOptions {
useVad?: boolean;
}
```

### CactusSTTDetectLanguageParams

```typescript
interface CactusSTTDetectLanguageParams {
audio: string | number[];
options?: CactusSTTDetectLanguageOptions;
}
```

### CactusSTTDetectLanguageResult

```typescript
interface CactusSTTDetectLanguageResult {
language: string;
confidence?: number;
}
```

### CactusVADParams

```typescript
Expand Down
2 changes: 1 addition & 1 deletion android/gradle.properties
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,4 @@ Cactus_kotlinVersion=2.0.21
Cactus_minSdkVersion=24
Cactus_targetSdkVersion=34
Cactus_compileSdkVersion=35
Cactus_ndkVersion=27.1.12297006
Cactus_ndkVersion=28.2.13676358
2 changes: 1 addition & 1 deletion example/android/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ buildscript {
minSdkVersion = 24
compileSdkVersion = 36
targetSdkVersion = 36
ndkVersion = "27.1.12297006"
ndkVersion = "28.2.13676358"
kotlinVersion = "2.1.20"
}
repositories {
Expand Down
4 changes: 2 additions & 2 deletions example/ios/Podfile.lock
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
PODS:
- boost (1.84.0)
- Cactus (1.10.0):
- Cactus (1.10.1):
- boost
- DoubleConversion
- fast_float
Expand Down Expand Up @@ -2643,7 +2643,7 @@ EXTERNAL SOURCES:

SPEC CHECKSUMS:
boost: 7e761d76ca2ce687f7cc98e698152abd03a18f90
Cactus: 88585f8a152312dcb391526d839133d72d054031
Cactus: 10a32b41ac3f49c1b47f75a7ade47145c38aceb9
DoubleConversion: cb417026b2400c8f53ae97020b2be961b59470cb
fast_float: b32c788ed9c6a8c584d114d0047beda9664e7cc6
FBLazyVector: b8f1312d48447cca7b4abc21ed155db14742bd03
Expand Down
20 changes: 19 additions & 1 deletion example/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import STTScreen from './STTScreen';
import StreamSTTScreen from './StreamSTTScreen';
import ChatScreen from './ChatScreen';
import IndexScreen from './IndexScreen';
import ModelBrowserScreen from './ModelBrowserScreen';

type Screen =
| 'Home'
Expand All @@ -23,7 +24,8 @@ type Screen =
| 'STT'
| 'StreamSTT'
| 'Chat'
| 'Index';
| 'Index'
| 'ModelBrowser';

const App = () => {
const [selectedScreen, setSelectedScreen] = useState<Screen>('Home');
Expand Down Expand Up @@ -60,6 +62,10 @@ const App = () => {
setSelectedScreen('Index');
};

const handleGoToModelBrowser = () => {
setSelectedScreen('ModelBrowser');
};

const renderScreen = () => {
switch (selectedScreen) {
case 'Completion':
Expand All @@ -76,6 +82,8 @@ const App = () => {
return <ChatScreen />;
case 'Index':
return <IndexScreen />;
case 'ModelBrowser':
return <ModelBrowserScreen />;
default:
return null;
}
Expand Down Expand Up @@ -158,6 +166,16 @@ const App = () => {
CactusIndex with embeddings
</Text>
</TouchableOpacity>

<TouchableOpacity
style={styles.menuButton}
onPress={handleGoToModelBrowser}
>
<Text style={styles.menuButtonTitle}>Model Browser</Text>
<Text style={styles.menuButtonDescription}>
Browse available models from the registry
</Text>
</TouchableOpacity>
</ScrollView>
</View>
</SafeAreaView>
Expand Down
206 changes: 206 additions & 0 deletions example/src/ModelBrowserScreen.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,206 @@
import { useEffect, useState } from 'react';
import {
View,
Text,
TextInput,
ScrollView,
StyleSheet,
ActivityIndicator,
} from 'react-native';
import { getRegistry, type CactusModel } from 'cactus-react-native';

type RegistryEntry = { key: string; model: CactusModel };

const ModelBrowserScreen = () => {
const [entries, setEntries] = useState<RegistryEntry[]>([]);
const [filtered, setFiltered] = useState<RegistryEntry[]>([]);
const [search, setSearch] = useState('');
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);

useEffect(() => {
getRegistry()
.then((registry) => {
const list = Object.entries(registry).map(([key, model]) => ({
key,
model,
}));
list.sort((a, b) => a.key.localeCompare(b.key));
setEntries(list);
setFiltered(list);
})
.catch((e: unknown) =>
setError(e instanceof Error ? e.message : String(e))
)
.finally(() => setLoading(false));
}, []);

useEffect(() => {
const q = search.trim().toLowerCase();
setFiltered(q ? entries.filter((e) => e.key.includes(q)) : entries);
}, [search, entries]);

if (loading) {
return (
<View style={styles.center}>
<ActivityIndicator size="large" />
<Text style={styles.loadingText}>Loading model registry...</Text>
</View>
);
}

if (error) {
return (
<View style={styles.center}>
<Text style={styles.errorText}>{error}</Text>
</View>
);
}

return (
<View style={styles.container}>
<TextInput
style={styles.search}
value={search}
onChangeText={setSearch}
placeholder="Search models..."
placeholderTextColor="#666"
clearButtonMode="while-editing"
/>
<Text style={styles.count}>
{filtered.length} of {entries.length} models
</Text>
<ScrollView contentContainerStyle={styles.list}>
{filtered.map(({ key, model }) => {
const { int4, int8 } = model.quantization;
return (
<View key={key} style={styles.card}>
<Text style={styles.modelName}>{key}</Text>
<View style={styles.variants}>
<VariantRow label="int4" url={int4.url} pro={int4.pro?.apple} />
<VariantRow label="int8" url={int8.url} pro={int8.pro?.apple} />
</View>
</View>
);
})}
</ScrollView>
</View>
);
};

const VariantRow = ({
label,
url,
pro,
}: {
label: string;
url: string;
pro?: string;
}) => (
<View style={styles.variantRow}>
<View style={styles.variantBadge}>
<Text style={styles.variantLabel}>{label}</Text>
</View>
<Text style={styles.variantUrl} numberOfLines={1}>
{url.replace('https://huggingface.co/', '')}
</Text>
{pro && (
<View style={styles.proBadge}>
<Text style={styles.proLabel}>Apple</Text>
</View>
)}
</View>
);

export default ModelBrowserScreen;

const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#fff',
},
center: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
padding: 20,
},
loadingText: {
marginTop: 12,
fontSize: 14,
color: '#666',
},
errorText: {
fontSize: 14,
color: '#c00',
textAlign: 'center',
},
search: {
margin: 16,
marginBottom: 8,
borderWidth: 1,
borderColor: '#ddd',
borderRadius: 8,
paddingHorizontal: 12,
paddingVertical: 10,
fontSize: 16,
color: '#000',
},
count: {
marginHorizontal: 16,
marginBottom: 8,
fontSize: 12,
color: '#666',
},
list: {
padding: 16,
paddingTop: 0,
},
card: {
backgroundColor: '#f3f3f3',
borderRadius: 8,
padding: 12,
marginBottom: 12,
},
modelName: {
fontSize: 15,
fontWeight: '600',
color: '#000',
marginBottom: 8,
},
variants: {
gap: 6,
},
variantRow: {
flexDirection: 'row',
alignItems: 'center',
gap: 8,
},
variantBadge: {
backgroundColor: '#000',
borderRadius: 4,
paddingHorizontal: 6,
paddingVertical: 2,
},
variantLabel: {
fontSize: 11,
color: '#fff',
fontWeight: '600',
},
variantUrl: {
flex: 1,
fontSize: 11,
color: '#666',
},
proBadge: {
backgroundColor: '#e8f0fe',
borderRadius: 4,
paddingHorizontal: 6,
paddingVertical: 2,
},
proLabel: {
fontSize: 11,
color: '#1a73e8',
fontWeight: '600',
},
});
Loading
Loading