// // Copyright 2022 Picovoice Inc. // // You may not use this file except in compliance with the license. A copy of the license is located in the "LICENSE" // file accompanying this source. // // Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on // an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the // specific language governing permissions and limitations under the License. // import React, {Component} from 'react'; import { EventSubscription, NativeEventEmitter, PermissionsAndroid, Platform, SafeAreaView, ScrollView, StatusBar, TouchableOpacity, } from 'react-native'; import {StyleSheet, Text, View} from 'react-native'; import {Leopard, LeopardErrors} from '@picovoice/leopard-react-native'; import { VoiceProcessor, BufferEmitter, } from '@picovoice/react-native-voice-processor'; import Recorder from './Recorder'; enum UIState { LOADING, INIT, RECORDING, PROCESSING, TRANSCRIBED, ERROR, } type Props = {}; type State = { appState: UIState; errorMessage: string | null; transcription: string; recordSeconds: number; processSeconds: number; }; export default class App extends Component<Props, State> { _leopard?: Leopard; _accessKey: string = '${YOUR_ACCESS_KEY_HERE}'; // AccessKey obtained from Picovoice Console (https://console.picovoice.ai/) _recorder: Recorder = new Recorder(); _voiceProcessor?: VoiceProcessor; _bufferListener?: EventSubscription; _bufferEmitter?: NativeEventEmitter; _recordInterval?: NodeJS.Timer; constructor(props: Props) { super(props); this.state = { appState: UIState.LOADING, errorMessage: null, transcription: '', recordSeconds: 0.0, processSeconds: 0.0, }; } componentDidMount() { this.init(); } componentWillUnmount() { if (this.state.appState === UIState.RECORDING) { this._stopProcessing(); } if (this._leopard !== undefined) { this._leopard.delete(); this._leopard = undefined; } } async init() { try { this._leopard = await Leopard.create( this._accessKey, 'leopard_params.pv', ); this._voiceProcessor = VoiceProcessor.getVoiceProcessor( 512, this._leopard.sampleRate, ); this._bufferEmitter = new NativeEventEmitter(BufferEmitter); this._bufferListener = this._bufferEmitter.addListener( BufferEmitter.BUFFER_EMITTER_KEY, async (buffer: number[]) => { if (this.state.appState !== UIState.ERROR) { try { await this._recorder.writeSamples(buffer); } catch { this.handleError('Failed to write to wav file.'); } } }, ); this.setState({ appState: UIState.INIT, }); } catch (err: any) { this.handleError(err); } } handleError(err: any) { let errorMessage = ''; if (err instanceof LeopardErrors.LeopardInvalidArgumentError) { errorMessage = `${err.message}\nPlease make sure accessKey ${this._accessKey} is a valid access key.`; } else if (err instanceof LeopardErrors.LeopardActivationError) { errorMessage = 'AccessKey activation error'; } else if (err instanceof LeopardErrors.LeopardActivationLimitError) { errorMessage = 'AccessKey reached its device limit'; } else if (err instanceof LeopardErrors.LeopardActivationRefusedError) { errorMessage = 'AccessKey refused'; } else if (err instanceof LeopardErrors.LeopardActivationThrottledError) { errorMessage = 'AccessKey has been throttled'; } else { errorMessage = err.toString(); } this._voiceProcessor?.stop(); this.setState({ appState: UIState.ERROR, errorMessage: errorMessage, }); } _startProcessing() { this.setState({ appState: UIState.RECORDING, recordSeconds: 0, }); let recordAudioRequest; if (Platform.OS === 'android') { recordAudioRequest = this._requestRecordAudioPermission(); } else { recordAudioRequest = new Promise(function (resolve, _) { resolve(true); }); } recordAudioRequest.then(async (hasPermission) => { if (!hasPermission) { this.handleError( 'Required permissions (Microphone) were not found. Please add to platform code.', ); return; } try { await this._recorder.resetFile(); await this._recorder.writeWavHeader(); await this._voiceProcessor?.start(); this._recordInterval = setInterval(() => { if (this.state.recordSeconds < 120 - 0.1) { this.setState({ recordSeconds: this.state.recordSeconds + 0.1, }); } else { this._stopProcessing(); } }, 100); } catch (err: any) { this.handleError(err); } }); } _stopProcessing() { this.setState({ appState: UIState.PROCESSING, }); clearInterval(this._recordInterval!); this._voiceProcessor?.stop().then(async () => { try { const audioPath = await this._recorder.finalize(); const start = Date.now(); const res = await this._leopard?.processFile(audioPath); const end = Date.now(); if (res !== undefined) { this.setState({ transcription: res, appState: UIState.TRANSCRIBED, processSeconds: (end - start) / 1000, }); } } catch (err: any) { this.handleError(err); } }); } _toggleListening() { if (this.state.appState === UIState.RECORDING) { this._stopProcessing(); } else if ( this.state.appState === UIState.INIT || this.state.appState === UIState.TRANSCRIBED ) { this._startProcessing(); } } async _requestRecordAudioPermission() { try { const granted = await PermissionsAndroid.request( PermissionsAndroid.PERMISSIONS.RECORD_AUDIO, { title: 'Microphone Permission', message: 'Leopard needs access to your microphone to record audio', buttonNegative: 'Cancel', buttonPositive: 'OK', }, ); return granted === PermissionsAndroid.RESULTS.GRANTED; } catch (err: any) { this.handleError(err); return false; } } render() { const disabled = this.state.appState === UIState.LOADING || this.state.appState === UIState.ERROR || this.state.appState === UIState.PROCESSING; return ( <SafeAreaView style={styles.container}> <StatusBar barStyle="dark-content" backgroundColor="#377DFF" /> <View style={styles.statusBar}> <Text style={styles.statusBarText}>Leopard</Text> </View> <View style={{flex: 6}}> <ScrollView style={styles.transcriptionBox}> <Text style={styles.transcriptionText}> {this.state.transcription} </Text> </ScrollView> </View> {this.state.appState === UIState.ERROR ? ( <View style={styles.errorBox}> <Text style={{ color: 'white', fontSize: 16, }}> {this.state.errorMessage} </Text> </View> ) : ( <View style={styles.stateContainer}> {this.state.appState === UIState.INIT && ( <Text style={{textAlign: 'center'}}> Record up to 2 minutes of audio to be transcribed by Leopard </Text> )} {this.state.appState === UIState.RECORDING && ( <Text> Recording: {this.state.recordSeconds.toFixed(1)} / 120 (seconds) </Text> )} {this.state.appState === UIState.PROCESSING && ( <Text>Processing audio...</Text> )} {this.state.appState === UIState.TRANSCRIBED && ( <Text> Transcribed {this.state.recordSeconds.toFixed(1)} seconds of audio in {this.state.processSeconds.toFixed(1)} seconds </Text> )} </View> )} <View style={{flex: 1, justifyContent: 'center', alignContent: 'center'}}> <TouchableOpacity style={[styles.buttonStyle, disabled ? styles.buttonDisabled : {}]} onPress={() => this._toggleListening()} disabled={disabled}> <Text style={styles.buttonText}> {this.state.appState === UIState.RECORDING ? 'Stop' : 'Start'} </Text> </TouchableOpacity> </View> <View style={{flex: 0.5, justifyContent: 'flex-end', paddingBottom: 10}}> <Text style={styles.instructions}> Made in Vancouver, Canada by Picovoice </Text> </View> </SafeAreaView> ); } } const styles = StyleSheet.create({ container: { flex: 1, flexDirection: 'column', justifyContent: 'center', backgroundColor: '#F5FCFF', }, subContainer: { flex: 1, justifyContent: 'center', }, statusBar: { flex: 1, backgroundColor: '#377DFF', justifyContent: 'flex-end', maxHeight: 50, }, statusBarText: { fontSize: 18, color: 'white', fontWeight: 'bold', marginLeft: 15, marginBottom: 15, }, buttonStyle: { width: '50%', height: '100%', alignSelf: 'center', justifyContent: 'center', backgroundColor: '#377DFF', borderRadius: 100, }, buttonText: { fontSize: 30, fontWeight: 'bold', color: 'white', textAlign: 'center', }, buttonDisabled: { backgroundColor: 'gray', }, itemStyle: { fontWeight: 'bold', fontSize: 20, textAlign: 'center', }, instructions: { textAlign: 'center', color: '#666666', }, errorBox: { backgroundColor: 'red', margin: 20, padding: 20, textAlign: 'center', }, stateContainer: { flex: 0.5, justifyContent: 'center', alignItems: 'center', padding: 10, }, transcriptionBox: { backgroundColor: '#25187E', margin: 20, marginBottom: 10, padding: 20, height: '100%', }, transcriptionText: { fontSize: 20, color: 'white', }, });