Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
8575eb4
feat: add resource fetcher adapters for Expo and bare React Native
rizalibnu Jan 23, 2026
d37d7a1
feat: add bare React Native LLM chat example app
rizalibnu Jan 23, 2026
109755b
Merge branch 'main' into feat/resource-fetcher-adapters
rizalibnu Jan 24, 2026
748290f
feat: enhance resource fetcher with error handling and new methods fo…
rizalibnu Jan 27, 2026
139926a
chore: exclude llm_bare app from workspace
rizalibnu Jan 27, 2026
d52df47
feat: integrate MMKV for persistent state storage in background downl…
rizalibnu Jan 27, 2026
916feb1
Merge remote-tracking branch 'upstream/main' into feat/resource-fetch…
rizalibnu Jan 27, 2026
8f6fa82
chore: temporarily remove bare RN LLM example app for code review
rizalibnu Jan 27, 2026
a9d16b4
chore: revert formatting in inference time and memory usage documenta…
rizalibnu Jan 27, 2026
68dab92
chore: remove bare app directories from .gitignore
rizalibnu Jan 27, 2026
529e49e
chore: update react-native-executorch dependency to allow any version
rizalibnu Jan 27, 2026
d324a74
docs: add bare and expo adapters with installation and usage instruct…
rizalibnu Jan 27, 2026
96150b9
chore: bump version to 0.8.0 in package.json
rizalibnu Jan 27, 2026
af4ca6a
chore: add react-native-executorch as a dependency to adapters
rizalibnu Jan 27, 2026
73d1957
chore: add RNFS to the spell check wordlist
rizalibnu Jan 27, 2026
3638069
chore: update Node version in .nvmrc and adjust typecheck scripts in …
rizalibnu Jan 27, 2026
359427b
chore: replace generic error with RnExecutorchError in ResourceFetche…
rizalibnu Jan 27, 2026
614835e
chore: move yarn prepare from adapter typecheck scripts to CI workflow
rizalibnu Jan 27, 2026
3db78aa
Merge branch 'main' into feat/resource-fetcher-adapters
rizalibnu Jan 29, 2026
de85d45
chore: update workspaces to use wildcard patterns for packages and apps
rizalibnu Jan 29, 2026
1dc8951
fix: handle directory creation errors in ResourceFetcherUtils
rizalibnu Jan 29, 2026
aa193e8
refactor: rename adapters to resource-fetcher packages
rizalibnu Jan 29, 2026
219e06f
refactor: add resetAdapter method to ResourceFetcher
rizalibnu Jan 30, 2026
43a1147
refactor: simplify ResourceFetcherAdapter interface
rizalibnu Jan 30, 2026
89ea1ea
build: configure TypeScript compilation for resource-fetcher packages
rizalibnu Jan 30, 2026
0104367
refactor: streamline Typecheck step in CI workflow
rizalibnu Jan 30, 2026
ac19ef5
refactor: update CI workflow and package scripts for improved type ch…
rizalibnu Jan 30, 2026
450a50d
Merge branch 'main' into feat/resource-fetcher-adapters
rizalibnu Feb 7, 2026
5f21035
refactor: extract download completion logic and improve type safety
rizalibnu Feb 8, 2026
9a0213f
feat: (Experimental) vision camera integration - array buffer
NorbertKlockiewicz Feb 11, 2026
a27b009
fix: correct frame data extraction
NorbertKlockiewicz Feb 11, 2026
0c00c24
feat: frame extractor for zero-copy approach
NorbertKlockiewicz Feb 12, 2026
caed39a
chore: num minSdkVersion to 26
NorbertKlockiewicz Feb 13, 2026
0908272
feat: unify frame extraction and preprocessing
NorbertKlockiewicz Feb 16, 2026
115e5f4
feat: remove unused bindJSIMethods
NorbertKlockiewicz Feb 16, 2026
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
1 change: 1 addition & 0 deletions .cspell-wordlist.txt
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,7 @@ Português
codegen
cstdint
ocurred
RNFS
libfbjni
libc
gradlew
Expand Down
8 changes: 3 additions & 5 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ jobs:
run: yarn lint

- name: Typecheck files
run: yarn typecheck
run: yarn workspaces foreach --all --topological-dev run prepare && yarn typecheck

build-library:
runs-on: ubuntu-latest
Expand All @@ -35,7 +35,5 @@ jobs:
- name: Setup
uses: ./.github/actions/setup

- name: Build package
run: |
cd packages/react-native-executorch
yarn prepare
- name: Build all packages
run: yarn workspaces foreach --all --topological-dev run prepare
2 changes: 1 addition & 1 deletion .nvmrc
Original file line number Diff line number Diff line change
@@ -1 +1 @@
v20
v22
16 changes: 14 additions & 2 deletions apps/computer-vision/app.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,8 @@
"bundleIdentifier": "com.anonymous.computervision",
"infoPlist": {
"NSCameraUsageDescription": "Process photo from camera"
}
},
"appleTeamId": "B357MU264T"
},
"android": {
"adaptiveIcon": {
Expand All @@ -30,6 +31,17 @@
"web": {
"favicon": "./assets/icons/favicon.png"
},
"plugins": ["expo-font", "expo-router"]
"plugins": [
"expo-font",
"expo-router",
[
"expo-build-properties",
{
"android": {
"minSdkVersion": 26
}
}
]
]
}
}
7 changes: 7 additions & 0 deletions apps/computer-vision/app/_layout.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
import { Drawer } from 'expo-router/drawer';
import { initExecutorch } from 'react-native-executorch';
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';
Expand All @@ -10,6 +13,10 @@ import {
} from '@react-navigation/drawer';
import { GeneratingContext } from '../context';

initExecutorch({
resourceFetcher: ExpoResourceFetcher,
});

interface CustomDrawerProps extends DrawerContentComponentProps {
isGenerating: boolean;
}
Expand Down
265 changes: 265 additions & 0 deletions apps/computer-vision/app/camera_object_detection/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,265 @@
import React, { useEffect, useMemo, useState } from 'react';
import {
View,
StyleSheet,
Text,
ActivityIndicator,
TouchableOpacity,
} from 'react-native';
import {
Camera,
useCameraDevices,
getCameraFormat,
Templates,
useFrameProcessor,
useCameraPermission,
type Frame,
} from 'react-native-vision-camera';
import { useResizePlugin } from 'vision-camera-resize-plugin';
import {
ObjectDetectionModule,
SSDLITE_320_MOBILENET_V3_LARGE,
} from 'react-native-executorch';
import ScreenWrapper from '../../ScreenWrapper';
import ColorPalette from '../../colors';

export default function CameraObjectDetectionScreen() {
// Model state
const detectionModel = useMemo(() => new ObjectDetectionModule(), []);
const [isModelReady, setIsModelReady] = useState(false);

// Screen dimensions

// Camera setup - V5 API
const devices = useCameraDevices();
const device = devices.find((d) => d.position === 'back') ?? devices[0];

const format = useMemo(() => {
if (device == null) return undefined;
return getCameraFormat(device, Templates.Video);
}, [device]);

const { hasPermission, requestPermission } = useCameraPermission();

// Resize plugin for efficient frame scaling
const { resize } = useResizePlugin();

// Load model
useEffect(() => {
(async () => {
try {
await detectionModel.load(SSDLITE_320_MOBILENET_V3_LARGE);
setIsModelReady(true);
} catch (error) {
console.error('Failed to load model:', error);
}
})();

return () => {
detectionModel.delete();
};
}, [detectionModel]);

// Frame processing with Vision Camera v4
const frameProcessor = useFrameProcessor(
(frame: Frame) => {
'worklet';

if (!isModelReady) {
return;
}

try {
// Use VisionCamera's resize plugin for better performance
const resized = resize(frame, {
scale: {
width: 640,
height: 640,
},
pixelFormat: 'rgb',
dataType: 'uint8',
});

// Prepare frame data for model
const frameData = {
data: resized.buffer,
width: 640,
height: 640,
};

// Run inference - generateFromFrame is JSI-bound for worklet compatibility
const result = detectionModel.generateFromFrame(frameData, 0.5);
console.log(result);
} catch (error: any) {
console.log(
'Frame processing error:',
error?.message || 'Unknown error'
);
}
},
[isModelReady, detectionModel, resize]
);

// Loading state
if (!isModelReady) {
return (
<ScreenWrapper>
<View style={styles.loadingContainer}>
<ActivityIndicator size="large" color={ColorPalette.strongPrimary} />
<Text style={styles.loadingText}>Loading model...</Text>
</View>
</ScreenWrapper>
);
}

// Permission request
if (!hasPermission) {
return (
<ScreenWrapper>
<View style={styles.permissionContainer}>
<Text style={styles.permissionText}>
Camera permission is required
</Text>
<TouchableOpacity
style={styles.permissionButton}
onPress={requestPermission}
>
<Text style={styles.permissionButtonText}>Grant Permission</Text>
</TouchableOpacity>
</View>
</ScreenWrapper>
);
}

// No camera device
if (device == null) {
return (
<ScreenWrapper>
<View style={styles.errorContainer}>
<Text style={styles.errorText}>No camera device found</Text>
</View>
</ScreenWrapper>
);
}

return (
<ScreenWrapper>
<View style={styles.container}>
{/* Camera View */}
<Camera
style={StyleSheet.absoluteFill}
device={device}
isActive={true}
frameProcessor={frameProcessor}
format={format}
pixelFormat="yuv"
/>
</View>
</ScreenWrapper>
);
}

const styles = StyleSheet.create({
container: {
flex: 1,
},
statsContainer: {
position: 'absolute',
top: 20,
right: 20,
flexDirection: 'row',
gap: 12,
},
statBox: {
backgroundColor: 'rgba(0, 0, 0, 0.7)',
paddingHorizontal: 16,
paddingVertical: 8,
borderRadius: 12,
alignItems: 'center',
minWidth: 70,
},
statLabel: {
color: '#888',
fontSize: 11,
fontWeight: '600',
textTransform: 'uppercase',
},
statValue: {
color: '#fff',
fontSize: 24,
fontWeight: 'bold',
marginTop: 2,
},
detectionList: {
position: 'absolute',
bottom: 20,
left: 20,
right: 20,
gap: 8,
},
detectionItem: {
backgroundColor: 'rgba(0, 0, 0, 0.8)',
paddingHorizontal: 16,
paddingVertical: 12,
borderRadius: 12,
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
borderLeftWidth: 4,
},
detectionLabel: {
color: 'white',
fontSize: 16,
fontWeight: '600',
textTransform: 'capitalize',
},
detectionScore: {
color: '#4ECDC4',
fontSize: 16,
fontWeight: 'bold',
},
loadingContainer: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
},
loadingText: {
marginTop: 16,
fontSize: 16,
color: ColorPalette.strongPrimary,
},
errorContainer: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
padding: 20,
},
errorText: {
fontSize: 16,
color: '#d32f2f',
textAlign: 'center',
},
permissionContainer: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
padding: 20,
},
permissionText: {
fontSize: 18,
color: ColorPalette.strongPrimary,
marginBottom: 20,
textAlign: 'center',
},
permissionButton: {
backgroundColor: ColorPalette.strongPrimary,
paddingHorizontal: 24,
paddingVertical: 12,
borderRadius: 8,
},
permissionButtonText: {
color: 'white',
fontSize: 16,
fontWeight: 'bold',
},
});
12 changes: 12 additions & 0 deletions apps/computer-vision/app/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,12 @@ export default function Home() {
>
<Text style={styles.buttonText}>Image Generation</Text>
</TouchableOpacity>
<TouchableOpacity
style={[styles.button, styles.cameraButton]}
onPress={() => router.navigate('camera_object_detection/')}
>
<Text style={styles.buttonText}>🎥 Camera Object Detection</Text>
</TouchableOpacity>
</View>
</View>
);
Expand Down Expand Up @@ -92,6 +98,12 @@ const styles = StyleSheet.create({
alignItems: 'center',
marginBottom: 10,
},
cameraButton: {
backgroundColor: '#2563eb',
},
testButton: {
backgroundColor: '#10b981',
},
buttonText: {
color: 'white',
fontSize: fontSizes.md,
Expand Down
Loading