Skip to content

Commit 1f8d700

Browse files
authored
feat: upgrade MLC-LLM and fix android demo app (#190)
* fix: download tensor-cache.json for new MLC-LLM version * fix: prevent eslint from linting dist dir * feat: demo app to support Android * fix(demo): icons on android * fix(demo): model selection if at least one chat is present * chore: comply with eslint
1 parent e3c83b4 commit 1f8d700

39 files changed

Lines changed: 423 additions & 113 deletions

apps/expo-example/app.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
"bundleIdentifier": "com.callstack.ai.example"
1818
},
1919
"android": {
20+
"forceDarkAllowed": false,
2021
"adaptiveIcon": {
2122
"foregroundImage": "./assets/adaptive-icon.png",
2223
"backgroundColor": "#ffffff"

apps/expo-example/package.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,11 @@
1313
"@azure/core-asynciterator-polyfill": "^1.0.2",
1414
"@bottom-tabs/react-navigation": "^0.10.2",
1515
"@expo/ui": "~0.2.0-beta.9",
16+
"@expo/vector-icons": "^15.0.3",
1617
"@lodev09/react-native-true-sheet": "^3.8.1",
1718
"@react-native-ai/apple": "workspace:*",
1819
"@react-native-ai/llama": "workspace:*",
20+
"@react-native-ai/mlc": "workspace:*",
1921
"@react-native-async-storage/async-storage": "^2.2.0",
2022
"@react-native-picker/picker": "2.11.1",
2123
"@react-navigation/bottom-tabs": "^7.9.0",
@@ -32,9 +34,11 @@
3234
"expo-clipboard": "~8.0.8",
3335
"expo-document-picker": "~14.0.8",
3436
"expo-file-system": "~19.0.21",
37+
"expo-font": "^14.0.11",
3538
"expo-glass-effect": "~0.1.8",
3639
"expo-status-bar": "~3.0.9",
3740
"expo-symbols": "~1.0.8",
41+
"expo-system-ui": "^6.0.9",
3842
"jotai": "^2.12.2",
3943
"llama.rn": "^0.10.1",
4044
"react": "19.1.0",

apps/expo-example/src/App.tsx

Lines changed: 30 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import Ionicons from '@expo/vector-icons/Ionicons'
12
import {
23
createDrawerNavigator,
34
DrawerContentComponentProps,
@@ -7,14 +8,14 @@ import { NavigationContainer } from '@react-navigation/native'
78
import { StatusBar } from 'expo-status-bar'
89
import { SymbolView } from 'expo-symbols'
910
import { Provider as JotaiProvider } from 'jotai'
10-
import React, { Suspense } from 'react'
11+
import React from 'react'
1112
import { Pressable, StyleSheet, Text, View } from 'react-native'
1213
import { GestureHandlerRootView } from 'react-native-gesture-handler'
1314
import { KeyboardProvider } from 'react-native-keyboard-controller'
1415
import { SafeAreaProvider } from 'react-native-safe-area-context'
1516

1617
import { AdaptiveGlass } from './components/AdaptiveGlass'
17-
import ChatScreen from './screens/apple/ChatScreen'
18+
import ChatScreen from './screens/ChatScreen'
1819
import { useChatStore } from './store/chatStore'
1920
import { colors } from './theme/colors'
2021

@@ -60,6 +61,9 @@ function CustomDrawerContent({ navigation }: DrawerContentComponentProps) {
6061
size={18}
6162
tintColor={colors.label}
6263
resizeMode="scaleAspectFit"
64+
fallback={
65+
<Ionicons name="add" size={18} color={colors.label} />
66+
}
6367
/>
6468
</Pressable>
6569
</AdaptiveGlass>
@@ -74,6 +78,13 @@ function CustomDrawerContent({ navigation }: DrawerContentComponentProps) {
7478
size={22}
7579
tintColor={colors.secondaryLabel}
7680
resizeMode="scaleAspectFit"
81+
fallback={
82+
<Ionicons
83+
name="chatbubble-outline"
84+
size={22}
85+
color={colors.secondaryLabel}
86+
/>
87+
}
7788
/>
7889
</View>
7990
</AdaptiveGlass>
@@ -110,6 +121,13 @@ function CustomDrawerContent({ navigation }: DrawerContentComponentProps) {
110121
size={18}
111122
tintColor={colors.tertiaryLabel}
112123
resizeMode="scaleAspectFit"
124+
fallback={
125+
<Ionicons
126+
name="chatbubble-outline"
127+
size={18}
128+
color={colors.tertiaryLabel}
129+
/>
130+
}
113131
/>
114132
<Text style={styles.chatItemTitle} numberOfLines={1}>
115133
{chat.title}
@@ -120,6 +138,13 @@ function CustomDrawerContent({ navigation }: DrawerContentComponentProps) {
120138
size={16}
121139
tintColor={colors.systemRed}
122140
resizeMode="scaleAspectFit"
141+
fallback={
142+
<Ionicons
143+
name="trash-outline"
144+
size={16}
145+
color={colors.systemRed}
146+
/>
147+
}
123148
/>
124149
</Pressable>
125150
</Pressable>
@@ -151,7 +176,7 @@ export default function App() {
151176
<Drawer.Screen name="Chat" component={ChatScreen} />
152177
</Drawer.Navigator>
153178
</NavigationContainer>
154-
<StatusBar style="auto" />
179+
<StatusBar style="dark" />
155180
</JotaiProvider>
156181
</SafeAreaProvider>
157182
</KeyboardProvider>
@@ -165,7 +190,7 @@ const styles = StyleSheet.create({
165190
},
166191
drawer: {
167192
width: 280,
168-
backgroundColor: colors.systemBackground as any,
193+
backgroundColor: colors.systemBackground,
169194
},
170195
drawerScroll: {
171196
flex: 1,
@@ -174,7 +199,7 @@ const styles = StyleSheet.create({
174199
paddingHorizontal: 16,
175200
paddingVertical: 16,
176201
borderBottomWidth: StyleSheet.hairlineWidth,
177-
borderBottomColor: colors.separator as any,
202+
borderBottomColor: colors.separator,
178203
},
179204
drawerHeaderRow: {
180205
flexDirection: 'row',

apps/expo-example/src/components/RecordButton.tsx renamed to apps/expo-example/src/components/RecordButton/RecordButtonUIBase.tsx

Lines changed: 27 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
import { apple } from '@react-native-ai/apple'
2-
import { experimental_transcribe } from 'ai'
1+
import Ionicons from '@expo/vector-icons/Ionicons'
2+
import { experimental_transcribe, TranscriptionModel } from 'ai'
33
import { SymbolView } from 'expo-symbols'
44
import { useRef, useState } from 'react'
55
import { ActivityIndicator, Alert, Pressable, StyleSheet } from 'react-native'
@@ -11,25 +11,27 @@ import {
1111
RecorderAdapterNode,
1212
} from 'react-native-audio-api'
1313

14-
import { colors } from '../theme/colors'
15-
import {
16-
float32ArrayToWAV,
17-
mergeBuffersToFloat32Array,
18-
} from '../utils/audioUtils'
19-
import { AdaptiveGlass } from './AdaptiveGlass'
14+
import { colors } from '../../theme/colors'
15+
import type { float32ArrayToWAV } from '../../utils/appleAudioUtils'
16+
import { mergeBuffersToFloat32Array } from '../../utils/audioUtils'
17+
import { AdaptiveGlass } from '../AdaptiveGlass'
2018

2119
const SAMPLE_RATE = 16000
2220
const STOP_DELAY_MS = 500
2321

24-
interface RecordButtonProps {
22+
export interface RecordButtonUIBaseProps {
2523
onTranscriptionComplete: (text: string) => void
2624
disabled?: boolean
25+
transcriptionModel?: TranscriptionModel
26+
float32ArrayToWAV?: typeof float32ArrayToWAV
2727
}
2828

29-
export function RecordButton({
29+
export function RecordButtonUIBase({
3030
onTranscriptionComplete,
3131
disabled = false,
32-
}: RecordButtonProps) {
32+
transcriptionModel,
33+
float32ArrayToWAV,
34+
}: RecordButtonUIBaseProps) {
3335
const [isRecording, setIsRecording] = useState(false)
3436
const [isStopping, setIsStopping] = useState(false)
3537
const [isTranscribing, setIsTranscribing] = useState(false)
@@ -66,11 +68,16 @@ export function RecordButton({
6668
}
6769

6870
const transcribe = async (audioBuffer: ArrayBuffer) => {
71+
if (!transcriptionModel) {
72+
console.error('transcriptionModel is not provided')
73+
return
74+
}
75+
6976
setIsTranscribing(true)
7077

7178
try {
7279
const result = await experimental_transcribe({
73-
model: apple.transcriptionModel(),
80+
model: transcriptionModel,
7481
audio: audioBuffer,
7582
})
7683

@@ -130,6 +137,11 @@ export function RecordButton({
130137
return
131138
}
132139

140+
if (!float32ArrayToWAV) {
141+
console.error('float32ArrayToWAV function is not provided')
142+
return
143+
}
144+
133145
// Immediately update UI to feel snappy
134146
setIsRecording(false)
135147
setIsStopping(true)
@@ -166,7 +178,7 @@ export function RecordButton({
166178
return (
167179
<Pressable
168180
onPress={handlePress}
169-
disabled={disabled}
181+
disabled={disabled || !transcriptionModel}
170182
style={styles.recordingButton}
171183
>
172184
<ActivityIndicator size="small" color="#fff" />
@@ -178,7 +190,7 @@ export function RecordButton({
178190
<AdaptiveGlass isInteractive style={styles.glassButton}>
179191
<Pressable
180192
onPress={handlePress}
181-
disabled={disabled || isProcessing}
193+
disabled={disabled || isProcessing || !transcriptionModel}
182194
style={styles.pressable}
183195
>
184196
{isProcessing ? (
@@ -192,6 +204,7 @@ export function RecordButton({
192204
size={20}
193205
tintColor={colors.label}
194206
resizeMode="scaleAspectFit"
207+
fallback={<Ionicons name="mic" size={20} color={colors.label} />}
195208
/>
196209
)}
197210
</Pressable>
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import {
2+
RecordButtonUIBase,
3+
type RecordButtonUIBaseProps,
4+
} from './RecordButtonUIBase'
5+
6+
export type RecordButtonProps = Omit<
7+
RecordButtonUIBaseProps,
8+
'transcriptionModel'
9+
>
10+
11+
export function RecordButton(props: RecordButtonProps) {
12+
return (
13+
<RecordButtonUIBase
14+
transcriptionModel={undefined}
15+
float32ArrayToWAV={undefined}
16+
{...props}
17+
/>
18+
)
19+
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import { apple } from '@react-native-ai/apple'
2+
import { useMemo } from 'react'
3+
4+
import { float32ArrayToWAV } from '../../utils/appleAudioUtils'
5+
import {
6+
RecordButtonUIBase,
7+
type RecordButtonUIBaseProps,
8+
} from './RecordButtonUIBase'
9+
10+
export type RecordButtonProps = Omit<
11+
RecordButtonUIBaseProps,
12+
'transcriptionModel' | 'float32ArrayToWAV'
13+
>
14+
15+
export function RecordButton(props: RecordButtonProps) {
16+
const transcriptionModel = useMemo(() => apple.transcriptionModel(), [])
17+
18+
return (
19+
<RecordButtonUIBase
20+
transcriptionModel={transcriptionModel}
21+
float32ArrayToWAV={float32ArrayToWAV}
22+
{...props}
23+
/>
24+
)
25+
}

apps/expo-example/src/components/adapters/appleSetupAdapter.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,13 @@ import type { LanguageModelV3, SpeechModelV3 } from '@ai-sdk/provider'
22
import { apple, createAppleProvider } from '@react-native-ai/apple'
33
import { ToolSet } from 'ai'
44

5-
import type { SetupAdapter } from '../../config/providers'
5+
import type { SetupAdapter } from '../../config/providers.common'
66

77
export const createAppleLanguageSetupAdapter = (
88
tools: ToolSet = {}
99
): SetupAdapter<LanguageModelV3> => {
1010
const apple = createAppleProvider({
11+
// @ts-expect-error
1112
availableTools: tools,
1213
})
1314
const model = apple.languageModel()

apps/expo-example/src/components/adapters/llamaModelSetupAdapter.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import {
77
getModelPath,
88
removeModel,
99
} from '../../../../../packages/llama/src/storage'
10-
import type { Availability, SetupAdapter } from '../../config/providers'
10+
import type { Availability, SetupAdapter } from '../../config/providers.common'
1111
import { isModelDownloaded } from '../../utils/storage'
1212

1313
export const createLlamaLanguageSetupAdapter = (

apps/expo-example/src/components/adapters/llamaSpeechSetupAdapter.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import {
66
removeModel,
77
} from '@react-native-ai/llama'
88

9-
import type { Availability, SetupAdapter } from '../../config/providers'
9+
import type { Availability, SetupAdapter } from '../../config/providers.common'
1010
import { isModelDownloaded } from '../../utils/storage'
1111

1212
interface LlamaSpeechSetupOptions {
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import type { LanguageModelV3 } from '@ai-sdk/provider'
2+
import { mlc } from '@react-native-ai/mlc'
3+
import RNBlobUtil from 'react-native-blob-util'
4+
5+
import type { Availability, SetupAdapter } from '../../config/providers.common'
6+
7+
export const createMLCLanguageSetupAdapter = (
8+
modelId: string
9+
): SetupAdapter<LanguageModelV3> => {
10+
const model = mlc.languageModel(modelId)
11+
12+
return {
13+
model,
14+
modelId: model.modelId,
15+
display: {
16+
label: model.modelId,
17+
accentColor: '#eb3c25',
18+
icon: 'cpu',
19+
},
20+
builtIn: false,
21+
async isAvailable(): Promise<Availability> {
22+
return (await RNBlobUtil.fs.exists(
23+
RNBlobUtil.fs.dirs.SDCardDir + `/${modelId}/tensor-cache.json`
24+
))
25+
? 'yes'
26+
: 'availableForDownload'
27+
},
28+
async download(onProgress) {
29+
await model.download(async (event) => {
30+
if (Number.isNaN(event.percentage)) {
31+
// handle broken partly-downloaded file
32+
onProgress(0)
33+
await model.remove()
34+
return
35+
}
36+
37+
onProgress(event.percentage)
38+
})
39+
},
40+
async delete() {
41+
await model.unload()
42+
await model.remove()
43+
},
44+
async unload() {
45+
await model.unload()
46+
},
47+
async prepare() {
48+
await model.prepare()
49+
},
50+
}
51+
}

0 commit comments

Comments
 (0)