@lakshmiprasanth/react-voice-to-text
Version:
A modern React package for voice-to-text conversion with real-time speech recognition and file upload support
747 lines (738 loc) • 25.7 kB
JavaScript
'use strict';
var react = require('react');
/**
* Simple EventEmitter implementation for browser compatibility
*/
class EventEmitter {
constructor() {
this.events = {};
}
on(event, listener) {
if (!this.events[event]) {
this.events[event] = [];
}
this.events[event].push(listener);
return this;
}
once(event, listener) {
const onceWrapper = (...args) => {
this.off(event, onceWrapper);
listener.apply(this, args);
};
return this.on(event, onceWrapper);
}
off(event, listener) {
if (!this.events[event])
return this;
this.events[event] = this.events[event].filter(l => l !== listener);
return this;
}
emit(event, ...args) {
if (!this.events[event])
return false;
this.events[event].forEach(listener => {
try {
listener.apply(this, args);
}
catch (error) {
console.error('Error in event listener:', error);
}
});
return true;
}
removeAllListeners(event) {
if (event) {
delete this.events[event];
}
else {
this.events = {};
}
return this;
}
}
/**
* Error types that can occur during speech recognition
*/
var SpeechRecognitionErrorType;
(function (SpeechRecognitionErrorType) {
SpeechRecognitionErrorType["NO_SPEECH"] = "no-speech";
SpeechRecognitionErrorType["ABORTED"] = "aborted";
SpeechRecognitionErrorType["AUDIO_CAPTURE"] = "audio-capture";
SpeechRecognitionErrorType["NETWORK"] = "network";
SpeechRecognitionErrorType["NOT_ALLOWED"] = "not-allowed";
SpeechRecognitionErrorType["SERVICE_NOT_ALLOWED"] = "service-not-allowed";
SpeechRecognitionErrorType["BAD_GRAMMAR"] = "bad-grammar";
SpeechRecognitionErrorType["LANGUAGE_NOT_SUPPORTED"] = "language-not-supported";
SpeechRecognitionErrorType["ENGINE_ERROR"] = "engine-error";
SpeechRecognitionErrorType["INVALID_CONFIG"] = "invalid-config";
})(SpeechRecognitionErrorType || (SpeechRecognitionErrorType = {}));
/**
* Speech recognition error
*/
class SpeechRecognitionError extends Error {
constructor(type, message, originalError) {
super(message);
this.type = type;
this.originalError = originalError;
this.name = 'SpeechRecognitionError';
}
}
/**
* Base class for speech recognition engines
*/
class BaseSpeechRecognitionEngine extends EventEmitter {
constructor() {
super();
this.isRecording = false;
this.config = {};
}
/**
* Validate configuration
*/
validateConfig(config) {
if (config.confidenceThreshold && (config.confidenceThreshold < 0 || config.confidenceThreshold > 1)) {
throw new SpeechRecognitionError(SpeechRecognitionErrorType.INVALID_CONFIG, 'Confidence threshold must be between 0 and 1');
}
if (config.maxAlternatives && config.maxAlternatives < 1) {
throw new SpeechRecognitionError(SpeechRecognitionErrorType.INVALID_CONFIG, 'Max alternatives must be at least 1');
}
if (config.sampleRate && config.sampleRate < 8000) {
throw new SpeechRecognitionError(SpeechRecognitionErrorType.INVALID_CONFIG, 'Sample rate must be at least 8000 Hz');
}
}
/**
* Validate audio input configuration
*/
validateAudioConfig(audioConfig) {
if (!audioConfig.source) {
throw new SpeechRecognitionError(SpeechRecognitionErrorType.INVALID_CONFIG, 'Audio source must be specified');
}
if (audioConfig.source === 'file' && !audioConfig.filePath) {
throw new SpeechRecognitionError(SpeechRecognitionErrorType.INVALID_CONFIG, 'File path must be specified for file source');
}
if (audioConfig.source === 'stream' && !audioConfig.audioStream) {
throw new SpeechRecognitionError(SpeechRecognitionErrorType.INVALID_CONFIG, 'Audio stream must be specified for stream source');
}
}
/**
* Emit error event with proper error handling
*/
emitError(type, message, originalError) {
const error = new SpeechRecognitionError(type, message, originalError);
this.emit('error', error);
}
/**
* Emit result event with validation
*/
emitResult(result) {
// Validate result
if (!result.transcript) {
return; // Skip empty results
}
// Apply confidence threshold if configured
if (this.config.confidenceThreshold && result.confidence < this.config.confidenceThreshold) {
return; // Skip low-confidence results
}
this.emit('result', result);
}
/**
* Get default configuration merged with provided config
*/
getConfig(config) {
return {
language: 'en-US',
sampleRate: 16000,
continuous: false,
interimResults: false,
maxAlternatives: 1,
confidenceThreshold: 0.0,
encoding: 'LINEAR16',
...this.config,
...config
};
}
/**
* Check if currently recording
*/
get isActive() {
return this.isRecording;
}
/**
* Set recording state
*/
setRecordingState(recording) {
this.isRecording = recording;
}
/**
* Clean up resources
*/
cleanup() {
this.removeAllListeners();
this.isRecording = false;
}
/**
* Type-safe event emitter methods
*/
emit(event, ...args) {
return super.emit(event, ...args);
}
on(event, listener) {
return super.on(event, listener);
}
once(event, listener) {
return super.once(event, listener);
}
off(event, listener) {
return super.off(event, listener);
}
}
/**
* Web Speech API engine for browser environments
*/
class WebSpeechEngine extends BaseSpeechRecognitionEngine {
constructor() {
super();
this.recognition = null;
this.mediaRecorder = null;
this.audioChunks = [];
}
/**
* Check if Web Speech API is available
*/
isAvailable() {
if (typeof window === 'undefined') {
return false; // Not in browser environment
}
return !!(window.SpeechRecognition || window.webkitSpeechRecognition);
}
/**
* Get supported languages (common languages supported by most browsers)
*/
getSupportedLanguages() {
return [
'en-US', 'en-GB', 'en-AU', 'en-CA', 'en-IN', 'en-NZ', 'en-ZA',
'es-ES', 'es-MX', 'es-AR', 'es-CO', 'es-CL', 'es-PE', 'es-VE',
'fr-FR', 'fr-CA', 'fr-CH', 'fr-BE',
'de-DE', 'de-AT', 'de-CH',
'it-IT', 'it-CH',
'pt-BR', 'pt-PT',
'ru-RU',
'ja-JP',
'ko-KR',
'zh-CN', 'zh-TW', 'zh-HK',
'ar-SA', 'ar-EG',
'hi-IN',
'th-TH',
'tr-TR',
'pl-PL',
'nl-NL', 'nl-BE',
'sv-SE',
'da-DK',
'no-NO',
'fi-FI'
];
}
/**
* Initialize the speech recognition engine
*/
async initialize(_config) {
if (!this.isAvailable()) {
throw new Error('Web Speech API is not available in this environment');
}
this.config = this.getConfig(_config);
this.validateConfig(this.config);
try {
await this.initializeRecognition();
this.emit('initialized');
}
catch (error) {
this.emitError(SpeechRecognitionErrorType.ENGINE_ERROR, `Failed to initialize Web Speech recognition: ${error instanceof Error ? error.message : 'Unknown error'}`, error instanceof Error ? error : undefined);
}
}
/**
* Start listening for speech
*/
async startListening(config) {
if (!this.recognition) {
throw new Error('Speech recognition not initialized. Call initialize() first.');
}
if (this.isRecording) {
await this.stopListening();
}
try {
this.recognition.start();
this.isRecording = true;
this.emit('started');
}
catch (error) {
this.emitError(SpeechRecognitionErrorType.ENGINE_ERROR, `Failed to start listening: ${error instanceof Error ? error.message : 'Unknown error'}`, error instanceof Error ? error : undefined);
}
}
/**
* Stop listening for speech
*/
async stopListening() {
if (!this.recognition) {
return;
}
try {
this.recognition.stop();
this.isRecording = false;
this.emit('stopped');
}
catch (error) {
this.emitError(SpeechRecognitionErrorType.ENGINE_ERROR, `Failed to stop listening: ${error instanceof Error ? error.message : 'Unknown error'}`, error instanceof Error ? error : undefined);
}
}
/**
* Start speech recognition
*/
async start(audioConfig, recognitionConfig) {
if (!this.isAvailable()) {
throw new Error('Web Speech API is not available in this environment');
}
this.validateAudioConfig(audioConfig);
this.config = this.getConfig(recognitionConfig);
this.validateConfig(this.config);
if (this.isRecording) {
await this.stop();
}
try {
await this.initializeRecognition();
if (audioConfig.source === 'microphone') {
await this.startMicrophoneRecognition(audioConfig);
}
else if (audioConfig.source === 'file') {
await this.processAudioFile(audioConfig.filePath);
}
else {
throw new Error('Web Speech API only supports microphone and file input');
}
}
catch (error) {
this.emitError(SpeechRecognitionErrorType.ENGINE_ERROR, `Failed to start Web Speech recognition: ${error instanceof Error ? error.message : 'Unknown error'}`, error instanceof Error ? error : undefined);
}
}
/**
* Initialize speech recognition instance
*/
async initializeRecognition() {
const SpeechRecognitionClass = window.SpeechRecognition || window.webkitSpeechRecognition;
this.recognition = new SpeechRecognitionClass();
// Configure recognition
this.recognition.continuous = this.config.continuous || false;
this.recognition.interimResults = this.config.interimResults || false;
this.recognition.lang = this.config.language || 'en-US';
this.recognition.maxAlternatives = this.config.maxAlternatives || 1;
// Set up event handlers
this.recognition.onstart = () => {
this.setRecordingState(true);
this.emit('start');
this.emit('audiostart');
};
this.recognition.onend = () => {
this.setRecordingState(false);
this.emit('audioend');
this.emit('end');
};
this.recognition.onresult = (event) => {
for (let i = event.resultIndex; i < event.results.length; i++) {
const result = event.results[i];
const alternative = result[0];
const speechResult = {
transcript: alternative.transcript,
confidence: alternative.confidence,
isFinal: result.isFinal,
alternatives: Array.from(result).map((alt) => ({
transcript: alt.transcript,
confidence: alt.confidence
}))
};
this.emitResult(speechResult);
}
};
this.recognition.onerror = (event) => {
let errorType;
switch (event.error) {
case 'no-speech':
errorType = SpeechRecognitionErrorType.NO_SPEECH;
break;
case 'aborted':
errorType = SpeechRecognitionErrorType.ABORTED;
break;
case 'audio-capture':
errorType = SpeechRecognitionErrorType.AUDIO_CAPTURE;
break;
case 'network':
errorType = SpeechRecognitionErrorType.NETWORK;
break;
case 'not-allowed':
errorType = SpeechRecognitionErrorType.NOT_ALLOWED;
break;
case 'service-not-allowed':
errorType = SpeechRecognitionErrorType.SERVICE_NOT_ALLOWED;
break;
case 'bad-grammar':
errorType = SpeechRecognitionErrorType.BAD_GRAMMAR;
break;
case 'language-not-supported':
errorType = SpeechRecognitionErrorType.LANGUAGE_NOT_SUPPORTED;
break;
default:
errorType = SpeechRecognitionErrorType.ENGINE_ERROR;
}
this.emitError(errorType, `Web Speech API error: ${event.error}`);
};
this.recognition.onspeechstart = () => this.emit('speechstart');
this.recognition.onspeechend = () => this.emit('speechend');
this.recognition.onsoundstart = () => this.emit('soundstart');
this.recognition.onsoundend = () => this.emit('soundend');
}
/**
* Start microphone recognition
*/
async startMicrophoneRecognition(audioConfig) {
if (!this.recognition) {
throw new Error('Recognition not initialized');
}
try {
// Request microphone permission and start recognition
const stream = await navigator.mediaDevices.getUserMedia({
audio: {
deviceId: audioConfig.deviceId ? { exact: audioConfig.deviceId } : undefined,
sampleRate: this.config.sampleRate || 16000
}
});
// Start recognition
this.recognition.start();
// If duration is specified, stop after that time
if (audioConfig.duration) {
setTimeout(() => {
this.stop().catch(console.error);
}, audioConfig.duration);
}
// Clean up stream when recognition ends
this.recognition.onend = () => {
stream.getTracks().forEach(track => track.stop());
this.setRecordingState(false);
this.emit('audioend');
this.emit('end');
};
}
catch (error) {
this.emitError(SpeechRecognitionErrorType.AUDIO_CAPTURE, `Failed to access microphone: ${error instanceof Error ? error.message : 'Unknown error'}`, error instanceof Error ? error : undefined);
}
}
/**
* Process audio file (requires MediaRecorder API)
*/
async processAudioFile(_filePath) {
// Note: Direct file processing with Web Speech API is limited
// This is a simplified implementation that would need additional audio processing
throw new Error('File processing with Web Speech API requires additional audio processing libraries');
}
/**
* Stop speech recognition
*/
async stop() {
if (this.recognition && this.isRecording) {
this.recognition.stop();
}
if (this.mediaRecorder && this.mediaRecorder.state === 'recording') {
this.mediaRecorder.stop();
}
this.setRecordingState(false);
}
/**
* Abort speech recognition
*/
async abort() {
if (this.recognition) {
this.recognition.abort();
}
if (this.mediaRecorder && this.mediaRecorder.state === 'recording') {
this.mediaRecorder.stop();
}
this.setRecordingState(false);
}
/**
* Process audio file directly (not supported by Web Speech API)
*/
async processFile(_file, _config) {
throw new Error('Direct file processing is not supported by Web Speech API. Use start() with file source instead.');
}
/**
* Process audio stream (not supported by Web Speech API)
*/
async processStream(_stream, _config) {
throw new Error('Direct stream processing is not supported by Web Speech API. Use start() with stream source instead.');
}
/**
* Get available audio input devices
*/
static async getAudioDevices() {
if (typeof navigator === 'undefined' || !navigator.mediaDevices) {
return [];
}
try {
const devices = await navigator.mediaDevices.enumerateDevices();
return devices.filter(device => device.kind === 'audioinput');
}
catch (error) {
console.error('Failed to enumerate audio devices:', error);
return [];
}
}
/**
* Check browser compatibility
*/
static getBrowserSupport() {
const features = [];
let supported = false;
if (typeof window !== 'undefined') {
if (window.SpeechRecognition || window.webkitSpeechRecognition) {
features.push('SpeechRecognition');
supported = true;
}
if (navigator.mediaDevices && typeof navigator.mediaDevices.getUserMedia === 'function') {
features.push('getUserMedia');
}
if (window.MediaRecorder) {
features.push('MediaRecorder');
}
}
return { supported, features };
}
}
/**
* Check browser support for speech recognition and related APIs
*/
function getBrowserSupport() {
if (typeof window === 'undefined') {
return {
webSpeechAPI: false,
mediaRecorder: false,
getUserMedia: false,
browser: {
name: 'unknown',
version: 'unknown'
}
};
}
const browserInfo = getBrowserInfo();
return {
webSpeechAPI: 'SpeechRecognition' in window || 'webkitSpeechRecognition' in window,
mediaRecorder: 'MediaRecorder' in window,
getUserMedia: 'mediaDevices' in navigator && 'getUserMedia' in navigator.mediaDevices,
browser: browserInfo
};
}
/**
* Get browser name and version
*/
function getBrowserInfo() {
if (typeof navigator === 'undefined') {
return { name: 'unknown', version: 'unknown' };
}
const userAgent = navigator.userAgent;
let name = 'unknown';
let version = 'unknown';
// Chrome
if (userAgent.includes('Chrome') && !userAgent.includes('Edg')) {
name = 'Chrome';
const match = userAgent.match(/Chrome\/(\d+)/);
if (match)
version = match[1];
}
// Safari
else if (userAgent.includes('Safari') && !userAgent.includes('Chrome')) {
name = 'Safari';
const match = userAgent.match(/Version\/(\d+)/);
if (match)
version = match[1];
}
// Firefox
else if (userAgent.includes('Firefox')) {
name = 'Firefox';
const match = userAgent.match(/Firefox\/(\d+)/);
if (match)
version = match[1];
}
// Edge
else if (userAgent.includes('Edg')) {
name = 'Edge';
const match = userAgent.match(/Edg\/(\d+)/);
if (match)
version = match[1];
}
return { name, version };
}
const useVoiceToText = (options = {}) => {
const [isInitialized, setIsInitialized] = react.useState(false);
const [isRecording, setIsRecording] = react.useState(false);
const [isProcessing, setIsProcessing] = react.useState(false);
const [results, setResults] = react.useState([]);
const [error, setError] = react.useState(null);
const [browserSupport, setBrowserSupport] = react.useState(null);
const [config, setConfig] = react.useState({
defaultRecognitionConfig: {
language: 'en-US',
continuous: true,
interimResults: true
},
debug: false,
...options
});
const [engine, setEngine] = react.useState(null);
// Initialize the engine
react.useEffect(() => {
const initialize = async () => {
try {
// Check browser support
const support = getBrowserSupport();
setBrowserSupport(support);
if (!support.webSpeechAPI) {
throw new Error('Speech recognition is not supported in this browser');
}
// Initialize Web Speech engine
const speechEngine = new WebSpeechEngine();
await speechEngine.initialize(config.defaultRecognitionConfig);
// Set up event listeners
speechEngine.on('result', (result) => {
setResults(prev => [...prev, result]);
options.onResult?.(result);
});
speechEngine.on('error', (error) => {
const errorMessage = error.message || 'Speech recognition error';
setError(errorMessage);
options.onError?.(errorMessage);
});
speechEngine.on('start', () => {
setIsRecording(true);
options.onStart?.();
});
speechEngine.on('end', () => {
setIsRecording(false);
options.onStop?.();
});
setEngine(speechEngine);
setIsInitialized(true);
}
catch (err) {
const errorMessage = err instanceof Error ? err.message : 'Failed to initialize voice recognition';
setError(errorMessage);
options.onError?.(errorMessage);
}
};
initialize();
// Cleanup
return () => {
if (engine) {
engine.cleanup();
}
};
}, []);
const startRecording = react.useCallback(async (recordingOptions) => {
if (!engine || !isInitialized) {
throw new Error('Voice recognition not initialized');
}
try {
setIsProcessing(true);
setError(null);
setResults([]);
const finalConfig = {
...config.defaultRecognitionConfig,
...recordingOptions
};
await engine.startListening(finalConfig);
}
catch (err) {
const errorMessage = err instanceof Error ? err.message : 'Failed to start recording';
setError(errorMessage);
options.onError?.(errorMessage);
throw err;
}
finally {
setIsProcessing(false);
}
}, [engine, isInitialized, config.defaultRecognitionConfig, options]);
const stopRecording = react.useCallback(async () => {
if (!engine) {
throw new Error('Voice recognition not initialized');
}
try {
setIsProcessing(true);
await engine.stopListening();
}
catch (err) {
const errorMessage = err instanceof Error ? err.message : 'Failed to stop recording';
setError(errorMessage);
options.onError?.(errorMessage);
throw err;
}
finally {
setIsProcessing(false);
}
}, [engine, options]);
const clearResults = react.useCallback(() => {
setResults([]);
}, []);
const clearError = react.useCallback(() => {
setError(null);
}, []);
const updateConfig = react.useCallback((newConfig) => {
setConfig(prev => ({ ...prev, ...newConfig }));
}, []);
const getConfig = react.useCallback(() => config, [config]);
return {
isInitialized,
isRecording,
isProcessing,
results,
error,
browserSupport,
startRecording,
stopRecording,
clearResults,
clearError,
updateConfig,
getConfig
};
};
const useVoiceRecorder = (options = {}) => {
const { language = 'en-US', continuous = true, interimResults = true, onResult, onError, onStart, onStop } = options;
const voiceToText = useVoiceToText({
defaultRecognitionConfig: {
language,
continuous,
interimResults
},
onResult,
onError,
onStart,
onStop
});
const startRecording = react.useCallback(async () => {
await voiceToText.startRecording({
language,
continuous,
interimResults
});
}, [voiceToText, language, continuous, interimResults]);
const stopRecording = react.useCallback(async () => {
await voiceToText.stopRecording();
}, [voiceToText]);
return {
isInitialized: voiceToText.isInitialized,
isRecording: voiceToText.isRecording,
isProcessing: voiceToText.isProcessing,
results: voiceToText.results,
error: voiceToText.error,
startRecording,
stopRecording,
clearResults: voiceToText.clearResults,
clearError: voiceToText.clearError
};
};
exports.useVoiceRecorder = useVoiceRecorder;
//# sourceMappingURL=useVoiceRecorder.js.map