2023-05-07 13:05:41 +02:00
|
|
|
import * as React from 'react';
|
|
|
|
import { useState, useEffect, useCallback } from 'react';
|
2023-08-18 10:42:03 +02:00
|
|
|
import { Banner, ActivityIndicator } from 'react-native-paper';
|
2023-06-13 19:06:54 +02:00
|
|
|
import { _, languageName } from '@joplin/lib/locale';
|
2023-05-07 13:05:41 +02:00
|
|
|
import useAsyncEffect, { AsyncEffectEvent } from '@joplin/lib/hooks/useAsyncEffect';
|
|
|
|
import { getVosk, Recorder, startRecording, Vosk } from '../../services/voiceTyping/vosk';
|
2023-06-02 16:44:00 +02:00
|
|
|
import { IconSource } from 'react-native-paper/lib/typescript/src/components/Icon';
|
2023-06-13 19:06:54 +02:00
|
|
|
import { modelIsDownloaded } from '../../services/voiceTyping/vosk.android';
|
2023-05-07 13:05:41 +02:00
|
|
|
|
|
|
|
interface Props {
|
2023-06-13 19:06:54 +02:00
|
|
|
locale: string;
|
2023-05-07 13:05:41 +02:00
|
|
|
onDismiss: ()=> void;
|
|
|
|
onText: (text: string)=> void;
|
|
|
|
}
|
|
|
|
|
|
|
|
enum RecorderState {
|
|
|
|
Loading = 1,
|
|
|
|
Recording = 2,
|
|
|
|
Processing = 3,
|
2023-06-11 17:13:36 +02:00
|
|
|
Error = 4,
|
2023-06-13 19:06:54 +02:00
|
|
|
Downloading = 5,
|
2023-05-07 13:05:41 +02:00
|
|
|
}
|
|
|
|
|
2023-06-13 19:06:54 +02:00
|
|
|
const useVosk = (locale: string): [Error | null, boolean, Vosk|null] => {
|
2023-05-07 13:05:41 +02:00
|
|
|
const [vosk, setVosk] = useState<Vosk>(null);
|
2023-06-11 17:13:36 +02:00
|
|
|
const [error, setError] = useState<Error>(null);
|
2023-06-13 19:06:54 +02:00
|
|
|
const [mustDownloadModel, setMustDownloadModel] = useState<boolean | null>(null);
|
2023-05-07 13:05:41 +02:00
|
|
|
|
|
|
|
useAsyncEffect(async (event: AsyncEffectEvent) => {
|
2023-06-13 19:06:54 +02:00
|
|
|
if (mustDownloadModel === null) return;
|
|
|
|
|
2023-06-11 17:13:36 +02:00
|
|
|
try {
|
2023-06-13 19:06:54 +02:00
|
|
|
const v = await getVosk(locale);
|
2023-06-11 17:13:36 +02:00
|
|
|
if (event.cancelled) return;
|
|
|
|
setVosk(v);
|
|
|
|
} catch (error) {
|
|
|
|
setError(error);
|
2023-06-13 19:06:54 +02:00
|
|
|
} finally {
|
|
|
|
setMustDownloadModel(false);
|
2023-06-11 17:13:36 +02:00
|
|
|
}
|
2023-06-13 19:06:54 +02:00
|
|
|
}, [locale, mustDownloadModel]);
|
|
|
|
|
|
|
|
useAsyncEffect(async (_event: AsyncEffectEvent) => {
|
|
|
|
setMustDownloadModel(!(await modelIsDownloaded(locale)));
|
|
|
|
}, [locale]);
|
2023-05-07 13:05:41 +02:00
|
|
|
|
2023-06-13 19:06:54 +02:00
|
|
|
return [error, mustDownloadModel, vosk];
|
2023-05-07 13:05:41 +02:00
|
|
|
};
|
|
|
|
|
|
|
|
export default (props: Props) => {
|
|
|
|
const [recorder, setRecorder] = useState<Recorder>(null);
|
|
|
|
const [recorderState, setRecorderState] = useState<RecorderState>(RecorderState.Loading);
|
2023-06-13 19:06:54 +02:00
|
|
|
const [voskError, mustDownloadModel, vosk] = useVosk(props.locale);
|
2023-05-07 13:05:41 +02:00
|
|
|
|
|
|
|
useEffect(() => {
|
2023-06-11 17:13:36 +02:00
|
|
|
if (voskError) {
|
|
|
|
setRecorderState(RecorderState.Error);
|
|
|
|
} else if (vosk) {
|
|
|
|
setRecorderState(RecorderState.Recording);
|
|
|
|
}
|
|
|
|
}, [vosk, voskError]);
|
2023-05-07 13:05:41 +02:00
|
|
|
|
2023-06-13 19:06:54 +02:00
|
|
|
useEffect(() => {
|
|
|
|
if (mustDownloadModel) {
|
|
|
|
setRecorderState(RecorderState.Downloading);
|
|
|
|
}
|
|
|
|
}, [mustDownloadModel]);
|
|
|
|
|
2023-05-07 13:05:41 +02:00
|
|
|
useEffect(() => {
|
|
|
|
if (recorderState === RecorderState.Recording) {
|
2023-06-02 16:44:00 +02:00
|
|
|
setRecorder(startRecording(vosk, {
|
|
|
|
onResult: (text: string) => {
|
|
|
|
props.onText(text);
|
|
|
|
},
|
|
|
|
}));
|
2023-05-07 13:05:41 +02:00
|
|
|
}
|
2023-06-02 16:44:00 +02:00
|
|
|
}, [recorderState, vosk, props.onText]);
|
2023-05-07 13:05:41 +02:00
|
|
|
|
|
|
|
const onDismiss = useCallback(() => {
|
2023-06-13 19:06:54 +02:00
|
|
|
if (recorder) recorder.cleanup();
|
2023-05-07 13:05:41 +02:00
|
|
|
props.onDismiss();
|
|
|
|
}, [recorder, props.onDismiss]);
|
|
|
|
|
|
|
|
const renderContent = () => {
|
2023-06-30 11:30:29 +02:00
|
|
|
// eslint-disable-next-line @typescript-eslint/ban-types -- Old code before rule was applied
|
2023-06-11 17:13:36 +02:00
|
|
|
const components: Record<RecorderState, Function> = {
|
|
|
|
[RecorderState.Loading]: () => _('Loading...'),
|
|
|
|
[RecorderState.Recording]: () => _('Please record your voice...'),
|
|
|
|
[RecorderState.Processing]: () => _('Converting speech to text...'),
|
2023-06-13 19:06:54 +02:00
|
|
|
[RecorderState.Downloading]: () => _('Downloading %s language files...', languageName(props.locale)),
|
2023-06-11 17:13:36 +02:00
|
|
|
[RecorderState.Error]: () => _('Error: %s', voskError.message),
|
2023-05-07 13:05:41 +02:00
|
|
|
};
|
|
|
|
|
2023-06-11 17:13:36 +02:00
|
|
|
return components[recorderState]();
|
2023-05-07 13:05:41 +02:00
|
|
|
};
|
|
|
|
|
2023-06-02 16:44:00 +02:00
|
|
|
const renderIcon = () => {
|
|
|
|
const components: Record<RecorderState, IconSource> = {
|
|
|
|
[RecorderState.Loading]: ({ size }: { size: number }) => <ActivityIndicator animating={true} style={{ width: size, height: size }} />,
|
|
|
|
[RecorderState.Recording]: 'microphone',
|
|
|
|
[RecorderState.Processing]: 'microphone',
|
2023-06-13 19:06:54 +02:00
|
|
|
[RecorderState.Downloading]: ({ size }: { size: number }) => <ActivityIndicator animating={true} style={{ width: size, height: size }} />,
|
2023-06-11 17:13:36 +02:00
|
|
|
[RecorderState.Error]: 'alert-circle-outline',
|
2023-05-07 13:05:41 +02:00
|
|
|
};
|
|
|
|
|
|
|
|
return components[recorderState];
|
|
|
|
};
|
|
|
|
|
|
|
|
return (
|
2023-08-18 10:42:03 +02:00
|
|
|
<Banner
|
|
|
|
visible={true}
|
|
|
|
icon={renderIcon()}
|
|
|
|
actions={[
|
|
|
|
{
|
|
|
|
label: _('Done'),
|
|
|
|
onPress: onDismiss,
|
|
|
|
},
|
|
|
|
]}>
|
|
|
|
{`${_('Voice typing...')}\n${renderContent()}`}
|
|
|
|
</Banner>
|
2023-05-07 13:05:41 +02:00
|
|
|
);
|
|
|
|
};
|