Skip to content

Commit

Permalink
wip
Browse files Browse the repository at this point in the history
  • Loading branch information
twitchard committed Oct 25, 2024
1 parent eb373a4 commit cb8d37a
Show file tree
Hide file tree
Showing 22 changed files with 9,229 additions and 0 deletions.
39 changes: 39 additions & 0 deletions evi-react-native-example/EVIExample/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
# Learn more https://docs.github.com/en/get-started/getting-started-with-git/ignoring-files

# dependencies
node_modules/

# Expo
.expo/
dist/
web-build/

# Native
*.orig.*
*.jks
*.p8
*.p12
*.key
*.mobileprovision
.env

# Metro
.metro-health-check*

# debug
npm-debug.*
yarn-debug.*
yarn-error.*

# macOS
.DS_Store
*.pem

# local env files
.env*.local

# typescript
*.tsbuildinfo

ios
android
1 change: 1 addition & 0 deletions evi-react-native-example/EVIExample/.npmrc
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
node-linker=hoisted
207 changes: 207 additions & 0 deletions evi-react-native-example/EVIExample/App.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,207 @@
import React, { useEffect, useState, useRef } from 'react';
import {
View,
Text,
Button,
StyleSheet,
ScrollView,
SafeAreaView,
} from 'react-native';
import { HumeClient, type Hume } from 'hume'

import * as NativeAudio from './modules/audio';

interface ChatEntry {
role: 'user' | 'assistant';
timestamp: string;
content: string;
}

const hume = new HumeClient({
apiKey: process.env.EXPO_PUBLIC_HUME_API_KEY || ''
})
const App = () => {
const [isConnected, setIsConnected] = useState(false);
const [isMuted, setIsMuted] = useState(false);
const [chatEntries, setChatEntries] = useState<ChatEntry[]>([
{ role: 'assistant', timestamp: new Date().toString(), content: 'Hello! How can I help you today?' },
{ role: 'user', timestamp: new Date().toString(), content: 'I am beyond help' },

]);
const [playbackQueue, setPlaybackQueue] = useState<any[]>([]);

const chatSocketRef = useRef<Hume.empathicVoice.chat.ChatSocket | null>(null);

useEffect(() => {
if (isConnected) {
NativeAudio.getPermissions().then(() => {
NativeAudio.startRecording();
}).catch((error) => {
console.error('Failed to get permissions:', error);
})
const chatSocket = hume.empathicVoice.chat.connect({
configId: process.env.EXPO_PUBLIC_HUME_CONFIG_ID,
})
chatSocket.on('message', handleIncomingMessage);

chatSocket.on('error', (error) => {
console.error("WebSocket Error:", error);
});

chatSocket.on('close', () => {
console.log("WebSocket Connection Closed");
setIsConnected(false);
});

chatSocketRef.current = chatSocket;

NativeAudio.onAudioInput(({base64EncodedAudio}: NativeAudio.AudioEventPayload) => {
chatSocket.sendAudioInput({data: base64EncodedAudio});
})
} else {
NativeAudio.stopRecording();
}
return () => {
NativeAudio.stopRecording();
chatSocketRef.current?.close();
}
}, [isConnected]);

const handleIncomingMessage = (message: any) => {
if (message.type === 'audio_output') {
const audioData = message.data;
const decodedAudio = atob(audioData);
playAudio(decodedAudio);
} else if (message.type === 'chat_message') {
const chatEntry: ChatEntry = {
role: message.role === 'assistant' ? 'assistant' : 'user',
timestamp: new Date().toString(),
content: message.content,
};
setChatEntries((prev) => [...prev, chatEntry]);
}
};

const connectToWebSocket = () => {
setIsConnected(true);
};

const disconnectFromWebSocket = () => {
if (chatSocketRef.current) {
chatSocketRef.current.close();
}
setIsConnected(false);
};

const muteInput = () => {
setIsMuted(true);
NativeAudio.stopRecording();
};

const unmuteInput = () => {
setIsMuted(false);
NativeAudio.startRecording();
};

const playAudio = (audioData: string) => {
if (playbackQueue.length > 0) {
setPlaybackQueue((prev) => [...prev, audioData]);
} else {
NativeAudio.playAudio(audioData);
}
};

return (
<View style={styles.appBackground}>
<SafeAreaView style={styles.container}>
<View style={styles.header}>
<Text style={styles.headerText}>You are {isConnected ? 'connected' : 'disconnected'}</Text>
</View>
<ScrollView style={styles.chatDisplay}>
{chatEntries.map((entry, index) => (
<View
key={index}
style={[
styles.chatEntry,
entry.role === 'user' ? styles.userChatEntry : styles.assistantChatEntry,
]}
>
<Text style={styles.chatText}>{entry.content}</Text>
</View>
))}
</ScrollView>
<View style={styles.buttonContainer}>
<Button
title={isConnected ? 'Disconnect' : 'Connect'}
onPress={isConnected ? disconnectFromWebSocket : connectToWebSocket}
/>
<Button title={isMuted ? 'Unmute' : 'Mute'} onPress={isMuted ? unmuteInput : muteInput} />
</View>
</SafeAreaView>
</View>
);
};

const styles = StyleSheet.create({
appBackground: {
flex: 1,
backgroundColor: 'rgb(255, 244, 232)',
alignItems: 'center',
},
container: {
flex: 1,
justifyContent: 'center',
padding: 16,
maxWidth: 600,
width: '100%'
},
header: {
marginBottom: 16,
alignItems: 'center',
},
headerText: {
fontSize: 18,
fontWeight: 'bold',
},
chatDisplay: {
flex: 1,
width: '100%',
marginBottom: 16,
},
chatEntry: {
padding: 10,
marginVertical: 5,
borderRadius: 15,
maxWidth: '75%',
shadowColor: '#000',
shadowOffset: {
width: 0,
height: 2,
},
shadowOpacity: 0.1,
shadowRadius: 2,
elevation: 3,
},
userChatEntry: {
backgroundColor: 'rgb(209, 226, 243)',
alignSelf: 'flex-end',
marginRight: 10,
},
assistantChatEntry: {
backgroundColor: '#fff',
alignSelf: 'flex-start',
marginLeft: 10,
},
chatText: {
fontSize: 16,
},
buttonContainer: {
flexDirection: 'row',
justifyContent: 'space-between',
width: '100%',
paddingHorizontal: 16,
paddingVertical: 8,
},
});

export default App;
32 changes: 32 additions & 0 deletions evi-react-native-example/EVIExample/app.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
{
"expo": {
"name": "EVIExample",
"slug": "EVIExample",
"version": "1.0.0",
"orientation": "portrait",
"icon": "./assets/icon.png",
"userInterfaceStyle": "light",
"splash": {
"image": "./assets/splash.png",
"resizeMode": "contain",
"backgroundColor": "#ffffff"
},
"ios": {
"supportsTablet": true,
"bundleIdentifier": "com.example.EVIExample",
"infoPlist": {
"NSMicrophoneUsageDescription": "This app uses the microphone to allow the user to talk to the EVI conversational AI interface"
}
},
"android": {
"adaptiveIcon": {
"foregroundImage": "./assets/adaptive-icon.png",
"backgroundColor": "#ffffff"
},
"package": "com.example.EVIExample"
},
"web": {
"favicon": "./assets/favicon.png"
}
}
}
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
6 changes: 6 additions & 0 deletions evi-react-native-example/EVIExample/babel.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
module.exports = function(api) {
api.cache(true);
return {
presets: ['babel-preset-expo'],
};
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
apply plugin: 'com.android.library'

group = 'expo.modules.audio'
version = '0.6.0'

def expoModulesCorePlugin = new File(project(":expo-modules-core").projectDir.absolutePath, "ExpoModulesCorePlugin.gradle")
apply from: expoModulesCorePlugin
applyKotlinExpoModulesCorePlugin()
useCoreDependencies()
useExpoPublishing()

// If you want to use the managed Android SDK versions from expo-modules-core, set this to true.
// The Android SDK versions will be bumped from time to time in SDK releases and may introduce breaking changes in your module code.
// Most of the time, you may like to manage the Android SDK versions yourself.
def useManagedAndroidSdkVersions = false
if (useManagedAndroidSdkVersions) {
useDefaultAndroidSdkVersions()
} else {
buildscript {
// Simple helper that allows the root project to override versions declared by this library.
ext.safeExtGet = { prop, fallback ->
rootProject.ext.has(prop) ? rootProject.ext.get(prop) : fallback
}
}
project.android {
compileSdkVersion safeExtGet("compileSdkVersion", 34)
defaultConfig {
minSdkVersion safeExtGet("minSdkVersion", 21)
targetSdkVersion safeExtGet("targetSdkVersion", 34)
}
}
}

android {
namespace "expo.modules.audio"
defaultConfig {
versionCode 1
versionName "0.6.0"
}
lintOptions {
abortOnError false
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
<manifest>
</manifest>
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package expo.modules.audio

import expo.modules.kotlin.modules.Module
import expo.modules.kotlin.modules.ModuleDefinition

class AudioModule : Module() {
// Each module class must implement the definition function. The definition consists of components
// that describes the module's functionality and behavior.
// See https://docs.expo.dev/modules/module-api for more details about available components.
override fun definition() = ModuleDefinition {
// Sets the name of the module that JavaScript code will use to refer to the module. Takes a string as an argument.
// Can be inferred from module's class name, but it's recommended to set it explicitly for clarity.
// The module will be accessible from `requireNativeModule('Audio')` in JavaScript.
Name("Audio")

// Sets constant properties on the module. Can take a dictionary or a closure that returns a dictionary.
Constants(
"PI" to Math.PI
)

// Defines event names that the module can send to JavaScript.
Events("onChange")

// Defines a JavaScript synchronous function that runs the native code on the JavaScript thread.
Function("hello") {
"Hello world! 👋"
}

// Defines a JavaScript function that always returns a Promise and whose native code
// is by default dispatched on the different thread than the JavaScript runtime runs on.
AsyncFunction("setValueAsync") { value: String ->
// Send an event to JavaScript.
sendEvent("onChange", mapOf(
"value" to value
))
}

// Enables the module to be used as a native view. Definition components that are accepted as part of
// the view definition: Prop, Events.
View(AudioView::class) {
// Defines a setter for the `name` prop.
Prop("name") { view: AudioView, prop: String ->
println(prop)
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"platforms": ["ios", "tvos", "android", "web"],
"ios": {
"modules": ["AudioModule"]
},
"android": {
"modules": ["expo.modules.audio.AudioModule"]
}
}
Loading

0 comments on commit cb8d37a

Please sign in to comment.