react-native-davoice-tts
Version:
tts library for React Native
203 lines (183 loc) • 6.39 kB
text/typescript
// stt/index.ts
import { NativeModules, NativeEventEmitter, Platform } from 'react-native';
// Prefer new names; gracefully fall back to legacy names during migration.
const STTNative =
NativeModules.STT ||
NativeModules.RCTSTT ||
NativeModules.Voice ||
NativeModules.RCTVoice;
if (STTNative) {
console.log('STTNative loaded:', Object.keys(STTNative));
} else {
console.error('STT native module is not linked correctly.');
}
type SpeechStartEvent = { /* keep your shape if you had TS types */ };
type SpeechEndEvent = {};
type SpeechRecognizedEvent = { isFinal: boolean };
type SpeechErrorEvent = { error: { code?: string; message?: string } };
type SpeechResultsEvent = { value: string[] };
type SpeechVolumeChangeEvent = { value: number };
type SpeechEvents = {
onSpeechStart?: (e: SpeechStartEvent) => void;
onSpeechRecognized?: (e: SpeechRecognizedEvent) => void;
onSpeechEnd?: (e: SpeechEndEvent) => void;
onSpeechError?: (e: SpeechErrorEvent) => void;
onSpeechResults?: (e: SpeechResultsEvent) => void;
onSpeechPartialResults?: (e: SpeechResultsEvent) => void;
onSpeechVolumeChanged?: (e: SpeechVolumeChangeEvent) => void;
};
const sttEmitter =
Platform.OS !== 'web' && STTNative ? new NativeEventEmitter(STTNative) : null;
type SpeechEventName = keyof Required<SpeechEvents>;
class RCTSTT {
private _loaded: boolean;
private _listeners: Array<{ remove: () => void }> | null;
private _events: Required<SpeechEvents>;
constructor() {
this._loaded = false;
this._listeners = null;
this._events = {
onSpeechStart: () => {},
onSpeechRecognized: () => {},
onSpeechEnd: () => {},
onSpeechError: () => {},
onSpeechResults: () => {},
onSpeechPartialResults: () => {},
onSpeechVolumeChanged: () => {},
};
}
/** Remove static handlers on the native module (parity with old Voice bridge). */
removeAllListeners() {
if (!STTNative) return;
STTNative.onSpeechStart = undefined;
STTNative.onSpeechRecognized = undefined;
STTNative.onSpeechEnd = undefined;
STTNative.onSpeechError = undefined;
STTNative.onSpeechResults = undefined;
STTNative.onSpeechPartialResults = undefined;
STTNative.onSpeechVolumeChanged = undefined;
}
/** Destroy/native teardown (parity with old API). */
destroy(): Promise<void> {
if (!this._loaded && !this._listeners) {
return Promise.resolve();
}
return new Promise((resolve, reject) => {
if (!STTNative?.destroySpeech) {
// Fallback: tolerate older native
this._listeners?.forEach(l => l.remove?.());
this._listeners = null;
resolve();
return;
}
STTNative.destroySpeech((error: string) => {
if (error) {
reject(new Error(error));
} else {
this._listeners?.forEach(l => l.remove?.());
this._listeners = null;
resolve();
}
});
});
}
/** Start recognition — mirrors old `.start(locale, options?)` */
start(locale: string, options: Record<string, any> = {}) {
if (!this._loaded && !this._listeners && sttEmitter) {
this._listeners = (Object.keys(this._events) as SpeechEventName[]).map(
(key: SpeechEventName) => sttEmitter.addListener(key, this._events[key]),
);
}
return new Promise<void>((resolve, reject) => {
const cb = (error: string) => (error ? reject(new Error(error)) : resolve());
if (Platform.OS === 'android' && STTNative?.startSpeech) {
// Keep Android parity with your previous voice bridge contract
STTNative.startSpeech(
locale,
{
EXTRA_LANGUAGE_MODEL: 'LANGUAGE_MODEL_FREE_FORM',
EXTRA_MAX_RESULTS: 5,
EXTRA_PARTIAL_RESULTS: true,
REQUEST_PERMISSIONS_AUTO: true,
...options,
},
cb,
);
} else if (STTNative?.startSpeech) {
STTNative.startSpeech(locale, cb);
} else if (STTNative?.start) {
STTNative.start(locale, cb);
} else {
reject(new Error('STTNative.startSpeech not available'));
}
});
}
stop(): Promise<void> {
if (!this._loaded && !this._listeners) return Promise.resolve();
return new Promise((resolve, reject) => {
if (!STTNative?.stopSpeech) return resolve();
STTNative.stopSpeech((error: string) => {
error ? reject(new Error(error)) : resolve();
});
});
}
cancel(): Promise<void> {
if (!this._loaded && !this._listeners) return Promise.resolve();
return new Promise((resolve, reject) => {
if (!STTNative?.cancelSpeech) return resolve();
STTNative.cancelSpeech((error: string) => {
error ? reject(new Error(error)) : resolve();
});
});
}
isAvailable(): Promise<0 | 1> {
return new Promise((resolve, reject) => {
if (!STTNative?.isSpeechAvailable) {
// Best effort fallback
resolve(1);
return;
}
STTNative.isSpeechAvailable((isAvailable: 0 | 1, error: string) => {
error ? reject(new Error(error)) : resolve(isAvailable);
});
});
}
isRecognizing(): Promise<0 | 1> {
return new Promise(resolve => {
if (!STTNative?.isRecognizing) return resolve(0);
STTNative.isRecognizing((isRecognizing: 0 | 1) => resolve(isRecognizing));
});
}
// Event setters (unchanged names so your app code stays the same)
set onSpeechStart(fn: (e: SpeechStartEvent) => void) {
this._events.onSpeechStart = fn;
}
set onSpeechRecognized(fn: (e: SpeechRecognizedEvent) => void) {
this._events.onSpeechRecognized = fn;
}
set onSpeechEnd(fn: (e: SpeechEndEvent) => void) {
this._events.onSpeechEnd = fn;
}
set onSpeechError(fn: (e: SpeechErrorEvent) => void) {
this._events.onSpeechError = fn;
}
set onSpeechResults(fn: (e: SpeechResultsEvent) => void) {
this._events.onSpeechResults = fn;
}
set onSpeechPartialResults(fn: (e: SpeechResultsEvent) => void) {
this._events.onSpeechPartialResults = fn;
}
set onSpeechVolumeChanged(fn: (e: SpeechVolumeChangeEvent) => void) {
this._events.onSpeechVolumeChanged = fn;
}
}
export type {
SpeechEndEvent,
SpeechErrorEvent,
SpeechEvents,
SpeechStartEvent,
SpeechRecognizedEvent,
SpeechResultsEvent,
SpeechVolumeChangeEvent,
};
export default new RCTSTT();