Skip to content
Draft
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
71 changes: 71 additions & 0 deletions apps/POST-PREBUILD.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
<!-- cspell:ignore prebuild Buildship getenv Podfile -->

# Post-prebuild patches

After running `npx expo prebuild --clean`, the following manual patches must be applied to the generated native directories. These work around issues in the Expo 55 / RN 0.83 monorepo setup that cannot be solved through config plugins yet.

## Android: Buildship stub cleanup in `settings.gradle`

**Affected apps:** computer-vision, llm, speech, text-embeddings

Gradle's Buildship plugin creates stub directories inside each app's `node_modules/` during project sync. These stubs shadow real packages hoisted to the workspace root, breaking autolinking.

In each app's `android/settings.gradle`, add the following block inside `extensions.configure(com.facebook.react.ReactSettingsExtension)`, **before** the `autolinkLibrariesFromCommand` call:

```groovy
extensions.configure(com.facebook.react.ReactSettingsExtension) { ex ->
// Remove Buildship/Gradle stubs that shadow real packages in the workspace-root node_modules.
def localNM = new File(rootDir, "../node_modules")
if (localNM.exists()) {
localNM.eachDir { dir ->
if (dir.name.startsWith("@")) {
dir.eachDir { scopedDir ->
if (!new File(scopedDir, "package.json").exists()) {
scopedDir.deleteDir()
}
}
} else if (!new File(dir, "package.json").exists()) {
dir.deleteDir()
}
}
}

if (System.getenv('EXPO_USE_COMMUNITY_AUTOLINKING') == '1') {
ex.autolinkLibrariesFromCommand()
} else {
ex.autolinkLibrariesFromCommand(expoAutolinking.rnConfigCommand)
}
}
```

## iOS: Stale pods after prebuild

**Affected apps:** all Expo apps

After prebuild, the generated `Podfile.lock` may reference stale versions (e.g. `React-Core-prebuilt 0.81.5` instead of `0.83.4`). Run a clean pod install:

```bash
cd apps/<app-name>/ios
rm -rf Pods Podfile.lock
pod install
```

## iOS: Buildship stubs breaking pod install

**Affected apps:** all Expo apps

The same Buildship stubs that affect Android also break iOS autolinking during `pod install`. Before running `pod install`, clean them:

```bash
# From the repo root
for app in apps/computer-vision apps/speech apps/llm apps/text-embeddings; do
find "$app/node_modules" -maxdepth 2 -type d ! -exec test -f '{}/package.json' \; -print 2>/dev/null | while read dir; do
# Skip the node_modules dir itself and scoped package parents
if [ "$(basename "$dir")" != "node_modules" ]; then
rm -rf "$dir"
fi
done
done
```

Alternatively, delete the entire `apps/<app-name>/node_modules/` directory before running `pod install` — yarn workspace hoisting means the app-local `node_modules` should only contain symlinks, not real packages.
43 changes: 42 additions & 1 deletion apps/computer-vision/app/_layout.tsx
Original file line number Diff line number Diff line change
@@ -1,22 +1,62 @@
import { Drawer } from 'expo-router/drawer';
import { initExecutorch } from 'react-native-executorch';

Check failure on line 2 in apps/computer-vision/app/_layout.tsx

View workflow job for this annotation

GitHub Actions / lint

Cannot find module 'react-native-executorch' or its corresponding type declarations.

Check failure on line 2 in apps/computer-vision/app/_layout.tsx

View workflow job for this annotation

GitHub Actions / build-library

Cannot find module 'react-native-executorch' or its corresponding type declarations.

Check failure on line 2 in apps/computer-vision/app/_layout.tsx

View workflow job for this annotation

GitHub Actions / build

Cannot find module 'react-native-executorch' or its corresponding type declarations.
import { ExpoResourceFetcher } from 'react-native-executorch-expo-resource-fetcher';

Check failure on line 3 in apps/computer-vision/app/_layout.tsx

View workflow job for this annotation

GitHub Actions / lint

Cannot find module 'react-native-executorch-expo-resource-fetcher' or its corresponding type declarations.

Check failure on line 3 in apps/computer-vision/app/_layout.tsx

View workflow job for this annotation

GitHub Actions / build-library

Cannot find module 'react-native-executorch-expo-resource-fetcher' or its corresponding type declarations.

Check failure on line 3 in apps/computer-vision/app/_layout.tsx

View workflow job for this annotation

GitHub Actions / build

Cannot find module 'react-native-executorch-expo-resource-fetcher' or its corresponding type declarations.

import ColorPalette from '../colors';
import React, { useState } from 'react';
import { Text, StyleSheet, View } from 'react-native';
import { Text, StyleSheet, View, TouchableOpacity } from 'react-native';

import {
DrawerContentComponentProps,
DrawerContentScrollView,
DrawerItemList,
} from '@react-navigation/drawer';
import { DrawerActions } from '@react-navigation/native';
import { useNavigation } from 'expo-router';
import Svg, { Rect } from 'react-native-svg';
import { GeneratingContext } from '../context';

initExecutorch({
resourceFetcher: ExpoResourceFetcher,
});

function HamburgerIcon({ tintColor }: { tintColor?: string }) {
const navigation = useNavigation();
return (
<TouchableOpacity
onPress={() => navigation.dispatch(DrawerActions.toggleDrawer())}
style={{ marginLeft: 12 }}

Check warning on line 28 in apps/computer-vision/app/_layout.tsx

View workflow job for this annotation

GitHub Actions / lint

Inline style: { marginLeft: 12 }
>
<Svg width={24} height={24} viewBox="0 0 24 24">
<Rect
x={2}
y={4}
width={20}
height={2}
rx={1}
fill={tintColor ?? '#000'}
/>
<Rect
x={2}
y={11}
width={20}
height={2}
rx={1}
fill={tintColor ?? '#000'}
/>
<Rect
x={2}
y={18}
width={20}
height={2}
rx={1}
fill={tintColor ?? '#000'}
/>
</Svg>
</TouchableOpacity>
);
}

interface CustomDrawerProps extends DrawerContentComponentProps {
isGenerating: boolean;
}
Expand Down Expand Up @@ -57,6 +97,7 @@
drawerInactiveTintColor: '#888',
headerTintColor: ColorPalette.primary,
headerTitleStyle: { color: ColorPalette.primary },
headerLeft: (props) => <HamburgerIcon tintColor={props.tintColor} />,
}}
>
<Drawer.Screen
Expand Down
11 changes: 6 additions & 5 deletions apps/computer-vision/app/vision_camera/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -118,12 +118,13 @@ const TASKS: Task[] = [
},
];

const frameKillSwitch = createSynchronizable(false);
const cameraPositionSync = createSynchronizable<'front' | 'back'>('back');

export default function VisionCameraScreen() {
const insets = useSafeAreaInsets();
const router = useRouter();
const [frameKillSwitch] = useState(() => createSynchronizable(false));
const [cameraPositionSync] = useState(() =>
createSynchronizable<'front' | 'back'>('back')
);
const [activeTask, setActiveTask] = useState<TaskId>('classification');
const [activeModel, setActiveModel] = useState<ModelId>('classification');
const [canvasSize, setCanvasSize] = useState({ width: 1, height: 1 });
Expand Down Expand Up @@ -163,11 +164,11 @@ export default function VisionCameraScreen() {
frameKillSwitch.setBlocking(false);
}, 300);
return () => clearTimeout(id);
}, [activeModel]);
}, [activeModel, frameKillSwitch]);

useEffect(() => {
cameraPositionSync.setBlocking(cameraPosition);
}, [cameraPosition]);
}, [cameraPosition, cameraPositionSync]);

const handleFpsChange = useCallback((newFps: number, newMs: number) => {
setFps(newFps);
Expand Down
8 changes: 3 additions & 5 deletions apps/computer-vision/components/BottomBar.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import ColorPalette from '../colors';
import FontAwesome from '@expo/vector-icons/FontAwesome';
import DeviceInfo from 'react-native-device-info';
import Constants from 'expo-constants';
import { View, TouchableOpacity, StyleSheet, Text } from 'react-native';
import { useSafeAreaInsets } from 'react-native-safe-area-context';

Expand All @@ -25,14 +25,12 @@ export const BottomBar = ({
<FontAwesome name="photo" size={24} color={ColorPalette.primary} />
</TouchableOpacity>
<TouchableOpacity
onPress={() =>
!DeviceInfo.isEmulatorSync() && handleCameraPress(true)
}
onPress={() => Constants.isDevice && handleCameraPress(true)}
>
<FontAwesome
name="camera"
size={24}
color={DeviceInfo.isEmulatorSync() ? '#888' : ColorPalette.primary}
color={Constants.isDevice ? ColorPalette.primary : '#888'}
/>
</TouchableOpacity>
</View>
Expand Down
47 changes: 24 additions & 23 deletions apps/computer-vision/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,40 +11,41 @@
"lint": "eslint . --ext .ts,.tsx --fix"
},
"dependencies": {
"@react-native/metro-config": "^0.81.5",
"@react-navigation/drawer": "^7.8.1",
"@react-navigation/native": "^7.1.28",
"@shopify/react-native-skia": "2.4.21",
"expo": "^54.0.27",
"expo-build-properties": "~1.0.10",
"expo-constants": "~18.0.11",
"expo-font": "~14.0.10",
"expo-linking": "~8.0.10",
"expo-router": "~6.0.17",
"expo-status-bar": "~3.0.9",
"metro-config": "^0.81.5",
"react": "19.1.0",
"react-native": "0.81.5",
"react-native-device-info": "^15.0.2",
"@expo/log-box": "~55.0.10",
"@react-native/metro-config": "^0.83.0",
"@react-navigation/drawer": "^7.9.4",
"@react-navigation/native": "^7.2.2",
"@shopify/react-native-skia": "2.6.2",
"expo": "^55.0.13",
"expo-build-properties": "~55.0.13",
"expo-constants": "~55.0.13",
"expo-font": "~55.0.6",
"expo-linking": "~55.0.12",
"expo-router": "~55.0.11",
"expo-status-bar": "~55.0.5",
"metro-config": "^0.83.0",
"react": "19.2.5",
"react-native": "0.83.4",
"react-native-executorch": "workspace:*",
"react-native-executorch-expo-resource-fetcher": "workspace:*",
"react-native-gesture-handler": "~2.28.0",
"react-native-image-picker": "^7.2.2",
"react-native-gesture-handler": "~2.31.1",
"react-native-image-picker": "^8.2.1",
"react-native-loading-spinner-overlay": "^3.0.1",
"react-native-nitro-image": "0.13.0",
"react-native-nitro-modules": "0.35.2",
"react-native-reanimated": "~4.2.2",
"react-native-safe-area-context": "~5.6.0",
"react-native-screens": "~4.16.0",
"react-native-svg": "15.15.3",
"react-native-reanimated": "~4.3.0",
"react-native-safe-area-context": "~5.7.0",
"react-native-screens": "~4.24.0",
"react-native-svg": "15.15.4",
"react-native-svg-transformer": "^1.5.3",
"react-native-vision-camera": "5.0.0-beta.7",
"react-native-worklets": "0.7.4"
"react-native-worklets": "0.8.1"
},
"devDependencies": {
"@babel/core": "^7.29.0",
"@types/pngjs": "^6.0.5",
"@types/react": "~19.2.0"
"@types/react": "~19.2.0",
"babel-preset-expo": "~55.0.16"
},
"private": true
}
47 changes: 42 additions & 5 deletions apps/llm/app/_layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,54 @@
import { ExpoResourceFetcher } from 'react-native-executorch-expo-resource-fetcher';
import ColorPalette from '../colors';
import React, { useState } from 'react';
import { Text, StyleSheet, View } from 'react-native';
import { Text, StyleSheet, View, TouchableOpacity } from 'react-native';
import {
DrawerContentComponentProps,
DrawerContentScrollView,
DrawerItemList,
DrawerToggleButton,
} from '@react-navigation/drawer';
import { DrawerActions } from '@react-navigation/native';
import { useNavigation } from 'expo-router';
import Svg, { Rect } from 'react-native-svg';
import { GeneratingContext } from '../context';

function HamburgerIcon({ tintColor }: { tintColor?: string }) {
const navigation = useNavigation();
return (
<TouchableOpacity
onPress={() => navigation.dispatch(DrawerActions.toggleDrawer())}
style={{ marginLeft: 12 }}

Check warning on line 22 in apps/llm/app/_layout.tsx

View workflow job for this annotation

GitHub Actions / lint

Inline style: { marginLeft: 12 }
>
<Svg width={24} height={24} viewBox="0 0 24 24">
<Rect
x={2}
y={4}
width={20}
height={2}
rx={1}
fill={tintColor ?? '#000'}
/>
<Rect
x={2}
y={11}
width={20}
height={2}
rx={1}
fill={tintColor ?? '#000'}
/>
<Rect
x={2}
y={18}
width={20}
height={2}
rx={1}
fill={tintColor ?? '#000'}
/>
</Svg>
</TouchableOpacity>
);
}

initExecutorch({
resourceFetcher: ExpoResourceFetcher,
});
Expand Down Expand Up @@ -56,9 +95,7 @@
drawerInactiveTintColor: '#888',
headerTintColor: ColorPalette.primary,
headerTitleStyle: { color: ColorPalette.primary },
headerLeft: () => (
<DrawerToggleButton tintColor={ColorPalette.primary} />
),
headerLeft: (props) => <HamburgerIcon tintColor={props.tintColor} />,
}}
>
<Drawer.Screen
Expand Down
19 changes: 8 additions & 11 deletions apps/llm/app/voice_chat/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -69,13 +69,7 @@ function VoiceChatScreen() {
useState<STTModelSources>(WHISPER_TINY_EN);
const [error, setError] = useState<string | null>(null);

const [recorder] = useState(
() =>
new AudioRecorder({
sampleRate: 16000,
bufferLengthInSamples: 1600,
})
);
const [recorder] = useState(() => new AudioRecorder());

const { setGlobalGenerating } = useContext(GeneratingContext);

Expand All @@ -92,7 +86,7 @@ function VoiceChatScreen() {
AudioManager.setAudioSessionOptions({
iosCategory: 'playAndRecord',
iosMode: 'spokenAudio',
iosOptions: ['allowBluetooth', 'defaultToSpeaker'],
iosOptions: ['allowBluetoothHFP', 'defaultToSpeaker'],
});
AudioManager.requestRecordingPermissions();
}, []);
Expand All @@ -106,9 +100,12 @@ function VoiceChatScreen() {
setIsRecording(true);
setLiveTranscription('');

recorder.onAudioReady(({ buffer }) => {
speechToText.streamInsert(buffer.getChannelData(0));
});
recorder.onAudioReady(
{ sampleRate: 16000, bufferLength: 1600, channelCount: 1 },
(event) => {
speechToText.streamInsert(event.buffer.getChannelData(0));
}
);
recorder.start();

let finalResult = '';
Expand Down
5 changes: 1 addition & 4 deletions apps/llm/metro.config.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,4 @@
const { getDefaultConfig } = require('expo/metro-config');
const {
wrapWithAudioAPIMetroConfig,
} = require('react-native-audio-api/metro-config');

const config = getDefaultConfig(__dirname);

Expand All @@ -19,4 +16,4 @@ config.resolver = {

config.resolver.assetExts.push('pte');

module.exports = wrapWithAudioAPIMetroConfig(config);
module.exports = config;
Loading
Loading