1
0
mirror of https://github.com/laurent22/joplin.git synced 2025-01-11 18:24:43 +02:00

Mobile: Write to note in realtime using voice typing

This commit is contained in:
Laurent Cozic 2023-06-02 15:44:00 +01:00
parent c6a9837f1f
commit 7779879c6d
4 changed files with 46 additions and 44 deletions

View File

@ -1244,6 +1244,8 @@ class NoteScreenComponent extends BaseScreenComponent {
}
const renderActionButton = () => {
if (this.state.voiceTypingDialogShown) return null;
const editButton = {
label: _('Edit'),
icon: 'md-create',
@ -1259,8 +1261,6 @@ class NoteScreenComponent extends BaseScreenComponent {
return <ActionButton mainButton={editButton} />;
};
const actionButtonComp = renderActionButton();
// Save button is not really needed anymore with the improved save logic
const showSaveButton = false; // this.state.mode === 'edit' || this.isModified() || this.saveButtonHasBeenShown_;
const saveButtonDisabled = true;// !this.isModified();
@ -1314,7 +1314,8 @@ class NoteScreenComponent extends BaseScreenComponent {
/>
{titleComp}
{bodyComponent}
{actionButtonComp}
{renderActionButton()}
{renderVoiceTypingDialog()}
<SelectDateTimeDialog themeId={this.props.themeId} shown={this.state.alarmDialogShown} date={dueDate} onAccept={this.onAlarmDialogAccept} onReject={this.onAlarmDialogReject} />
@ -1324,7 +1325,6 @@ class NoteScreenComponent extends BaseScreenComponent {
}}
/>
{noteTagDialog}
{renderVoiceTypingDialog()}
</View>
);
}

View File

@ -1,10 +1,10 @@
import * as React from 'react';
import { useState, useEffect, useCallback } from 'react';
import { Button, Dialog, Text } from 'react-native-paper';
import { Banner, ActivityIndicator } from 'react-native-paper';
import { _ } from '@joplin/lib/locale';
import useAsyncEffect, { AsyncEffectEvent } from '@joplin/lib/hooks/useAsyncEffect';
import { getVosk, Recorder, startRecording, Vosk } from '../../services/voiceTyping/vosk';
import { Alert } from 'react-native';
import { IconSource } from 'react-native-paper/lib/typescript/src/components/Icon';
interface Props {
onDismiss: ()=> void;
@ -42,58 +42,50 @@ export default (props: Props) => {
useEffect(() => {
if (recorderState === RecorderState.Recording) {
setRecorder(startRecording(vosk));
setRecorder(startRecording(vosk, {
onResult: (text: string) => {
props.onText(text);
},
}));
}
}, [recorderState, vosk]);
}, [recorderState, vosk, props.onText]);
const onDismiss = useCallback(() => {
recorder.cleanup();
props.onDismiss();
}, [recorder, props.onDismiss]);
const onStop = useCallback(async () => {
try {
setRecorderState(RecorderState.Processing);
const result = await recorder.stop();
props.onText(result);
} catch (error) {
Alert.alert(error.message);
}
onDismiss();
}, [recorder, onDismiss, props.onText]);
const renderContent = () => {
const components: Record<RecorderState, any> = {
[RecorderState.Loading]: <Text variant="bodyMedium">{_('Loading...')}</Text>,
[RecorderState.Recording]: <Text variant="bodyMedium">{_('Please record your voice...')}</Text>,
[RecorderState.Processing]: <Text variant="bodyMedium">{_('Converting speech to text...')}</Text>,
const components: Record<RecorderState, string> = {
[RecorderState.Loading]: _('Loading...'),
[RecorderState.Recording]: _('Please record your voice...'),
[RecorderState.Processing]: _('Converting speech to text...'),
};
return components[recorderState];
};
const renderActions = () => {
const components: Record<RecorderState, any> = {
[RecorderState.Loading]: null,
[RecorderState.Recording]: (
<Dialog.Actions>
<Button onPress={onDismiss}>{_('Cancel')}</Button>
<Button onPress={onStop}>{_('Done')}</Button>
</Dialog.Actions>
),
[RecorderState.Processing]: null,
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',
};
return components[recorderState];
};
return (
<Dialog visible={true} onDismiss={props.onDismiss}>
<Dialog.Title>{_('Voice typing')}</Dialog.Title>
<Dialog.Content>
{renderContent()}
</Dialog.Content>
{renderActions()}
</Dialog>
<Banner
visible={true}
icon={renderIcon()}
actions={[
{
label: _('Done'),
onPress: onDismiss,
},
]}>
{`${_('Voice typing...')}\n${renderContent()}`}
</Banner>
);
};

View File

@ -7,6 +7,10 @@ enum State {
Recording,
}
interface StartOptions {
onResult: (text: string)=> void;
}
let vosk_: Vosk|null = null;
let state_: State = State.Idle;
@ -26,7 +30,7 @@ export const getVosk = async () => {
return vosk_;
};
export const startRecording = (vosk: Vosk): Recorder => {
export const startRecording = (vosk: Vosk, options: StartOptions): Recorder => {
if (state_ !== State.Idle) throw new Error('Vosk is already recording');
state_ = State.Recording;
@ -56,8 +60,10 @@ export const startRecording = (vosk: Vosk): Recorder => {
};
eventHandlers.push(vosk.onResult(e => {
logger.info('Result', e.data);
result.push(e.data);
const text = e.data;
logger.info('Result', text);
result.push(text);
options.onResult(text);
}));
eventHandlers.push(vosk.onError(e => {

View File

@ -4,6 +4,10 @@ type Vosk = any;
export { Vosk };
interface StartOptions {
onResult: (text: string)=> void;
}
export interface Recorder {
stop: ()=> Promise<string>;
cleanup: ()=> void;
@ -13,7 +17,7 @@ export const getVosk = async () => {
return {} as any;
};
export const startRecording = (_vosk: Vosk): Recorder => {
export const startRecording = (_vosk: Vosk, _options: StartOptions): Recorder => {
return {
stop: async () => { return ''; },
cleanup: () => {},