UNPKG

@lakshmiprasanth/react-voice-to-text

Version:

A modern React package for voice-to-text conversion with real-time speech recognition and file upload support

709 lines (702 loc) 24.5 kB
import { useState, useEffect, useCallback } from '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] = useState(false); const [isRecording, setIsRecording] = useState(false); const [isProcessing, setIsProcessing] = useState(false); const [results, setResults] = useState([]); const [error, setError] = useState(null); const [browserSupport, setBrowserSupport] = useState(null); const [config, setConfig] = useState({ defaultRecognitionConfig: { language: 'en-US', continuous: true, interimResults: true }, debug: false, ...options }); const [engine, setEngine] = useState(null); // Initialize the engine 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 = 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 = 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 = useCallback(() => { setResults([]); }, []); const clearError = useCallback(() => { setError(null); }, []); const updateConfig = useCallback((newConfig) => { setConfig(prev => ({ ...prev, ...newConfig })); }, []); const getConfig = useCallback(() => config, [config]); return { isInitialized, isRecording, isProcessing, results, error, browserSupport, startRecording, stopRecording, clearResults, clearError, updateConfig, getConfig }; }; export { useVoiceToText }; //# sourceMappingURL=useVoiceToText.esm.js.map