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

1,129 lines (1,095 loc) â€ĸ 49 kB
import { jsx, jsxs } from 'react/jsx-runtime'; import { createContext, useState, useEffect, useContext, useRef } 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 VoiceToTextContext = createContext(null); const VoiceToTextProvider = ({ children, options = {}, onResult, onError, onStart, onStop }) => { 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); 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]); onResult?.(result); }); speechEngine.on('error', (error) => { const errorMessage = error.message || 'Speech recognition error'; setError(errorMessage); onError?.(errorMessage); }); speechEngine.on('start', () => { setIsRecording(true); onStart?.(); }); speechEngine.on('end', () => { setIsRecording(false); onStop?.(); }); setEngine(speechEngine); setIsInitialized(true); } catch (err) { const errorMessage = err instanceof Error ? err.message : 'Failed to initialize voice recognition'; setError(errorMessage); onError?.(errorMessage); } }; initialize(); // Cleanup return () => { if (engine) { engine.cleanup(); } }; }, []); const startRecording = 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); onError?.(errorMessage); throw err; } finally { setIsProcessing(false); } }; const stopRecording = 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); onError?.(errorMessage); throw err; } finally { setIsProcessing(false); } }; const clearResults = () => { setResults([]); }; const clearError = () => { setError(null); }; const updateConfig = (newConfig) => { setConfig(prev => ({ ...prev, ...newConfig })); }; const getConfig = () => config; const contextValue = { isInitialized, isRecording, isProcessing, results, error, browserSupport, startRecording, stopRecording, clearResults, clearError, updateConfig, getConfig }; return (jsx(VoiceToTextContext.Provider, { value: contextValue, children: children })); }; const useVoiceToTextContext = () => { const context = useContext(VoiceToTextContext); if (!context) { throw new Error('useVoiceToTextContext must be used within a VoiceToTextProvider'); } return context; }; function getDefaultExportFromCjs (x) { return x && x.__esModule && Object.prototype.hasOwnProperty.call(x, 'default') ? x['default'] : x; } var classnames = {exports: {}}; /*! Copyright (c) 2018 Jed Watson. Licensed under the MIT License (MIT), see http://jedwatson.github.io/classnames */ (function (module) { /* global define */ (function () { var hasOwn = {}.hasOwnProperty; function classNames () { var classes = ''; for (var i = 0; i < arguments.length; i++) { var arg = arguments[i]; if (arg) { classes = appendClass(classes, parseValue(arg)); } } return classes; } function parseValue (arg) { if (typeof arg === 'string' || typeof arg === 'number') { return arg; } if (typeof arg !== 'object') { return ''; } if (Array.isArray(arg)) { return classNames.apply(null, arg); } if (arg.toString !== Object.prototype.toString && !arg.toString.toString().includes('[native code]')) { return arg.toString(); } var classes = ''; for (var key in arg) { if (hasOwn.call(arg, key) && arg[key]) { classes = appendClass(classes, key); } } return classes; } function appendClass (value, newClass) { if (!newClass) { return value; } if (value) { return value + ' ' + newClass; } return value + newClass; } if (module.exports) { classNames.default = classNames; module.exports = classNames; } else { window.classNames = classNames; } }()); } (classnames)); var classnamesExports = classnames.exports; var classNames = /*@__PURE__*/getDefaultExportFromCjs(classnamesExports); const SUPPORTED_LANGUAGES = [ { code: 'en-US', name: 'English (US)' }, { code: 'en-GB', name: 'English (UK)' }, { code: 'es-ES', name: 'Spanish' }, { code: 'fr-FR', name: 'French' }, { code: 'de-DE', name: 'German' }, { code: 'it-IT', name: 'Italian' }, { code: 'pt-BR', name: 'Portuguese (Brazil)' }, { code: 'ja-JP', name: 'Japanese' }, { code: 'ko-KR', name: 'Korean' }, { code: 'zh-CN', name: 'Chinese (Simplified)' }, { code: 'zh-TW', name: 'Chinese (Traditional)' }, { code: 'ru-RU', name: 'Russian' }, { code: 'ar-SA', name: 'Arabic' }, { code: 'hi-IN', name: 'Hindi' }, { code: 'th-TH', name: 'Thai' }, { code: 'vi-VN', name: 'Vietnamese' }, { code: 'tr-TR', name: 'Turkish' }, { code: 'pl-PL', name: 'Polish' }, { code: 'nl-NL', name: 'Dutch' }, { code: 'sv-SE', name: 'Swedish' }, { code: 'da-DK', name: 'Danish' }, { code: 'no-NO', name: 'Norwegian' }, { code: 'fi-FI', name: 'Finnish' }, { code: 'cs-CZ', name: 'Czech' }, { code: 'hu-HU', name: 'Hungarian' }, { code: 'ro-RO', name: 'Romanian' }, { code: 'bg-BG', name: 'Bulgarian' }, { code: 'hr-HR', name: 'Croatian' }, { code: 'sk-SK', name: 'Slovak' }, { code: 'sl-SI', name: 'Slovenian' }, { code: 'et-EE', name: 'Estonian' }, { code: 'lv-LV', name: 'Latvian' }, { code: 'lt-LT', name: 'Lithuanian' }, { code: 'mt-MT', name: 'Maltese' }, { code: 'el-GR', name: 'Greek' }, { code: 'he-IL', name: 'Hebrew' }, { code: 'id-ID', name: 'Indonesian' }, { code: 'ms-MY', name: 'Malay' }, { code: 'fil-PH', name: 'Filipino' }, { code: 'uk-UA', name: 'Ukrainian' }, { code: 'be-BY', name: 'Belarusian' }, { code: 'kk-KZ', name: 'Kazakh' }, { code: 'ky-KG', name: 'Kyrgyz' }, { code: 'uz-UZ', name: 'Uzbek' }, { code: 'mn-MN', name: 'Mongolian' }, { code: 'ka-GE', name: 'Georgian' }, { code: 'hy-AM', name: 'Armenian' }, { code: 'az-AZ', name: 'Azerbaijani' }, { code: 'fa-IR', name: 'Persian' }, { code: 'ur-PK', name: 'Urdu' }, { code: 'bn-BD', name: 'Bengali' }, { code: 'si-LK', name: 'Sinhala' }, { code: 'my-MM', name: 'Burmese' }, { code: 'km-KH', name: 'Khmer' }, { code: 'lo-LA', name: 'Lao' }, { code: 'ne-NP', name: 'Nepali' }, { code: 'gu-IN', name: 'Gujarati' }, { code: 'pa-IN', name: 'Punjabi' }, { code: 'te-IN', name: 'Telugu' }, { code: 'kn-IN', name: 'Kannada' }, { code: 'ml-IN', name: 'Malayalam' }, { code: 'ta-IN', name: 'Tamil' }, { code: 'mr-IN', name: 'Marathi' }, { code: 'or-IN', name: 'Odia' }, { code: 'as-IN', name: 'Assamese' }, { code: 'sa-IN', name: 'Sanskrit' }, { code: 'am-ET', name: 'Amharic' }, { code: 'sw-KE', name: 'Swahili' }, { code: 'yo-NG', name: 'Yoruba' }, { code: 'ig-NG', name: 'Igbo' }, { code: 'ha-NG', name: 'Hausa' }, { code: 'zu-ZA', name: 'Zulu' }, { code: 'af-ZA', name: 'Afrikaans' }, { code: 'xh-ZA', name: 'Xhosa' }, { code: 'st-ZA', name: 'Southern Sotho' }, { code: 'tn-ZA', name: 'Tswana' }, { code: 'ss-ZA', name: 'Swati' }, { code: 've-ZA', name: 'Venda' }, { code: 'ts-ZA', name: 'Tsonga' }, { code: 'nr-ZA', name: 'Southern Ndebele' }, { code: 'nd-ZA', name: 'Northern Ndebele' } ]; const LanguageSelector = ({ value = 'en-US', onChange, disabled = false, className, showLabel = true, label = 'Language', placeholder = 'Select language...', ...props }) => { const handleChange = (event) => { onChange?.(event.target.value); }; return (jsxs("div", { className: classNames('language-selector', className), ...props, children: [showLabel && (jsx("label", { className: "language-selector__label", children: label })), jsxs("select", { value: value, onChange: handleChange, disabled: disabled, className: "language-selector__select", children: [jsx("option", { value: "", disabled: true, children: placeholder }), SUPPORTED_LANGUAGES.map((language) => (jsx("option", { value: language.code, children: language.name }, language.code)))] })] })); }; const RecordingControls = ({ isRecording, isProcessing, onStart, onStop, disabled = false, className, showStatus = true, startText = '🎤 Start Recording', stopText = 'âšī¸ Stop Recording', processingText = '🔄 Processing...', ...props }) => { const handleStart = () => { if (!disabled && !isRecording && !isProcessing) { onStart?.(); } }; const handleStop = () => { if (!disabled && isRecording && !isProcessing) { onStop?.(); } }; const getStatusText = () => { if (isProcessing) return processingText; if (isRecording) return '🔴 Recording...'; return 'Ready to record'; }; const getStatusColor = () => { if (isProcessing) return '#f39c12'; if (isRecording) return '#e74c3c'; return '#27ae60'; }; return (jsxs("div", { className: classNames('recording-controls', className), ...props, children: [jsx("div", { className: "recording-controls__buttons", children: !isRecording ? (jsx("button", { onClick: handleStart, disabled: disabled || isProcessing, className: classNames('recording-controls__button', 'recording-controls__button--start', { 'recording-controls__button--disabled': disabled || isProcessing }), children: startText })) : (jsx("button", { onClick: handleStop, disabled: disabled || isProcessing, className: classNames('recording-controls__button', 'recording-controls__button--stop', { 'recording-controls__button--disabled': disabled || isProcessing }), children: stopText })) }), showStatus && (jsxs("div", { className: "recording-controls__status", children: [jsx("div", { className: "recording-controls__status-indicator", style: { backgroundColor: getStatusColor() } }), jsx("span", { className: "recording-controls__status-text", children: getStatusText() })] }))] })); }; const ResultsDisplay = ({ results, error, onClear, showClearButton = true, showConfidence = true, showStatus = true, maxResults = 50, className, emptyText = 'No results yet. Start recording or convert a file to see results.', ...props }) => { const handleClear = () => { onClear?.(); }; const formatConfidence = (confidence) => { return `${(confidence * 100).toFixed(1)}%`; }; const getStatusIcon = (isFinal) => { return isFinal ? '✅' : 'âŗ'; }; const getStatusText = (isFinal) => { return isFinal ? 'Final' : 'Interim'; }; const getStatusColor = (isFinal) => { return isFinal ? '#27ae60' : '#f39c12'; }; const displayedResults = results.slice(-maxResults); if (error) { return (jsxs("div", { className: classNames('results-display', 'results-display--error', className), ...props, children: [jsxs("div", { className: "results-display__header", children: [jsx("h3", { children: "\u274C Error" }), showClearButton && (jsx("button", { onClick: handleClear, className: "results-display__clear-button", children: "Clear" }))] }), jsx("div", { className: "results-display__error", children: jsx("p", { children: error }) })] })); } if (results.length === 0) { return (jsx("div", { className: classNames('results-display', 'results-display--empty', className), ...props, children: jsxs("div", { className: "results-display__empty", children: [jsx("div", { className: "results-display__empty-icon", children: "\uD83D\uDCDD" }), jsx("h3", { children: "Results" }), jsx("p", { children: emptyText })] }) })); } return (jsxs("div", { className: classNames('results-display', className), ...props, children: [jsxs("div", { className: "results-display__header", children: [jsxs("h3", { children: ["\uD83D\uDCDD Results (", results.length, ")"] }), showClearButton && (jsx("button", { onClick: handleClear, className: "results-display__clear-button", children: "Clear All" }))] }), jsxs("div", { className: "results-display__content", children: [jsx("div", { className: "results-display__list", children: displayedResults.map((result, index) => (jsx("div", { className: classNames('results-display__item', { 'results-display__item--final': result.isFinal, 'results-display__item--interim': !result.isFinal }), children: jsxs("div", { className: "results-display__item-content", children: [jsx("div", { className: "results-display__text", children: result.transcript }), jsxs("div", { className: "results-display__meta", children: [showStatus && (jsxs("div", { className: "results-display__status", style: { color: getStatusColor(result.isFinal) }, children: [jsx("span", { className: "results-display__status-icon", children: getStatusIcon(result.isFinal) }), jsx("span", { className: "results-display__status-text", children: getStatusText(result.isFinal) })] })), showConfidence && (jsxs("div", { className: "results-display__confidence", children: ["Confidence: ", formatConfidence(result.confidence)] })), jsxs("div", { className: "results-display__index", children: ["#", results.length - displayedResults.length + index + 1] })] })] }) }, `${result.transcript}-${index}`))) }), results.length > maxResults && (jsx("div", { className: "results-display__overflow", children: jsxs("p", { children: ["Showing last ", maxResults, " results of ", results.length, " total"] }) }))] })] })); }; const VoiceRecorder = ({ className, showLanguageSelector = true, showResults = true, showControls = true, language = 'en-US', continuous = true, interimResults = true, onResult: _onResult, onError, onStart, onStop, children, ...props }) => { const { isInitialized, isRecording, isProcessing, results, error, startRecording, stopRecording, clearResults, clearError } = useVoiceToTextContext(); const [selectedLanguage, setSelectedLanguage] = useState(language); const handleStartRecording = async () => { try { await startRecording({ language: selectedLanguage, continuous, interimResults }); onStart?.(); } catch (err) { onError?.(err instanceof Error ? err.message : 'Failed to start recording'); } }; const handleStopRecording = async () => { try { await stopRecording(); onStop?.(); } catch (err) { onError?.(err instanceof Error ? err.message : 'Failed to stop recording'); } }; const handleLanguageChange = (newLanguage) => { setSelectedLanguage(newLanguage); }; const handleClearResults = () => { clearResults(); }; const handleClearError = () => { clearError(); }; if (!isInitialized) { return (jsx("div", { className: classNames('voice-recorder', 'voice-recorder--loading', className), ...props, children: jsxs("div", { className: "voice-recorder__status", children: [jsx("div", { className: "voice-recorder__loading-spinner" }), jsx("p", { children: "Initializing voice recognition..." })] }) })); } if (error) { return (jsx("div", { className: classNames('voice-recorder', 'voice-recorder--error', className), ...props, children: jsxs("div", { className: "voice-recorder__error", children: [jsx("h3", { children: "\u274C Error" }), jsx("p", { children: error }), jsx("button", { onClick: handleClearError, className: "voice-recorder__error-button", children: "Try Again" })] }) })); } return (jsxs("div", { className: classNames('voice-recorder', className), ...props, children: [jsxs("div", { className: "voice-recorder__header", children: [jsx("h2", { children: "\uD83C\uDFA4 Voice Recorder" }), jsx("p", { children: "Record your voice and see real-time transcription" })] }), jsxs("div", { className: "voice-recorder__content", children: [showLanguageSelector && (jsx("div", { className: "voice-recorder__language", children: jsx(LanguageSelector, { value: selectedLanguage, onChange: handleLanguageChange, disabled: isRecording || isProcessing }) })), showControls && (jsx("div", { className: "voice-recorder__controls", children: jsx(RecordingControls, { isRecording: isRecording, isProcessing: isProcessing, onStart: handleStartRecording, onStop: handleStopRecording, disabled: !isInitialized }) })), showResults && (jsx("div", { className: "voice-recorder__results", children: jsx(ResultsDisplay, { results: results, error: error, onClear: handleClearResults, showClearButton: results.length > 0 }) })), children && (jsx("div", { className: "voice-recorder__children", children: children }))] })] })); }; const FileUpload = ({ className, onFileSelect, onConvert, onError, acceptedFormats = ['audio/*'], maxFileSize = 50 * 1024 * 1024, // 50MB showLanguageSelector = true, language = 'en-US', dragAndDrop = true, multiple = false, disabled = false, children, ...props }) => { const [selectedFile, setSelectedFile] = useState(null); const [isConverting, setIsConverting] = useState(false); const [isDragOver, setIsDragOver] = useState(false); const [selectedLanguage, setSelectedLanguage] = useState(language); const fileInputRef = useRef(null); const handleFileSelect = (file) => { // Validate file size if (file.size > maxFileSize) { const error = `File size (${(file.size / 1024 / 1024).toFixed(2)}MB) exceeds maximum allowed size (${(maxFileSize / 1024 / 1024).toFixed(2)}MB)`; onError?.(error); return; } // Validate file type const isValidType = acceptedFormats.some(format => { if (format === 'audio/*') { return file.type.startsWith('audio/'); } return file.type === format; }); if (!isValidType) { const error = `File type ${file.type} is not supported. Please select an audio file.`; onError?.(error); return; } setSelectedFile(file); onFileSelect?.(file); }; const handleFileInputChange = (event) => { const files = event.target.files; if (files && files.length > 0) { handleFileSelect(files[0]); } }; const handleDragOver = (event) => { event.preventDefault(); setIsDragOver(true); }; const handleDragLeave = (event) => { event.preventDefault(); setIsDragOver(false); }; const handleDrop = (event) => { event.preventDefault(); setIsDragOver(false); const files = event.dataTransfer.files; if (files && files.length > 0) { handleFileSelect(files[0]); } }; const handleConvert = async () => { if (!selectedFile) return; setIsConverting(true); try { await onConvert?.(selectedFile, selectedLanguage); } catch (error) { onError?.(error instanceof Error ? error.message : 'Failed to convert file'); } finally { setIsConverting(false); } }; const handleClearFile = () => { setSelectedFile(null); if (fileInputRef.current) { fileInputRef.current.value = ''; } }; const handleLanguageChange = (newLanguage) => { setSelectedLanguage(newLanguage); }; const openFileDialog = () => { fileInputRef.current?.click(); }; const formatFileSize = (bytes) => { if (bytes === 0) return '0 Bytes'; const k = 1024; const sizes = ['Bytes', 'KB', 'MB', 'GB']; const i = Math.floor(Math.log(bytes) / Math.log(k)); return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]; }; return (jsxs("div", { className: classNames('file-upload', className), ...props, children: [jsxs("div", { className: "file-upload__header", children: [jsx("h3", { children: "\uD83D\uDCC1 File Upload & Convert" }), jsx("p", { children: "Upload an audio file to convert speech to text" })] }), jsxs("div", { className: "file-upload__content", children: [showLanguageSelector && (jsx("div", { className: "file-upload__language", children: jsx(LanguageSelector, { value: selectedLanguage, onChange: handleLanguageChange, disabled: disabled || isConverting }) })), jsxs("div", { className: "file-upload__upload-area", children: [dragAndDrop ? (jsx("div", { className: classNames('file-upload__drop-zone', { 'file-upload__drop-zone--drag-over': isDragOver, 'file-upload__drop-zone--disabled': disabled }), onDragOver: handleDragOver, onDragLeave: handleDragLeave, onDrop: handleDrop, onClick: !disabled ? openFileDialog : undefined, children: jsxs("div", { className: "file-upload__drop-zone-content", children: [jsx("div", { className: "file-upload__icon", children: "\uD83D\uDCC1" }), jsx("p", { className: "file-upload__drop-text", children: isDragOver ? 'Drop your audio file here' : 'Click to select or drag & drop audio file' }), jsx("p", { className: "file-upload__drop-hint", children: "Supported formats: MP3, WAV, OGG, FLAC, M4A" }), jsxs("p", { className: "file-upload__drop-hint", children: ["Max size: ", formatFileSize(maxFileSize)] })] }) })) : (jsxs("div", { className: "file-upload__input-wrapper", children: [jsx("input", { ref: fileInputRef, type: "file", accept: acceptedFormats.join(','), onChange: handleFileInputChange, disabled: disabled, multiple: multiple, className: "file-upload__input" }), jsx("button", { onClick: openFileDialog, disabled: disabled, className: "file-upload__select-button", children: "Select Audio File" })] })), jsx("input", { ref: fileInputRef, type: "file", accept: acceptedFormats.join(','), onChange: handleFileInputChange, disabled: disabled, multiple: multiple, className: "file-upload__hidden-input" })] }), selectedFile && (jsxs("div", { className: "file-upload__file-info", children: [jsxs("div", { className: "file-upload__file-details", children: [jsx("div", { className: "file-upload__file-name", children: selectedFile.name }), jsx("div", { className: "file-upload__file-size", children: formatFileSize(selectedFile.size) })] }), jsx("button", { onClick: handleClearFile, disabled: isConverting, className: "file-upload__clear-button", children: "\u2715" })] })), jsx("div", { className: "file-upload__controls", children: jsx("button", { onClick: handleConvert, disabled: !selectedFile || disabled || isConverting, className: "file-upload__convert-button", children: isConverting ? '🔄 Converting...' : '🔄 Convert File' }) }), children && (jsx("div", { className: "file-upload__children", children: children }))] })] })); }; const VoiceToTextConverter = ({ className, showFileUpload = true, showVoiceRecorder = true, showResults = true, defaultLanguage = 'en-US', onResult, onError, onStart, onStop, children }) => { const [activeTab, setActiveTab] = useState('recorder'); const [fileResults, setFileResults] = useState([]); const handleFileConvert = async (file, _language) => { try { // For file conversion, we'll use a simple approach with Web Speech API // In a real implementation, you might want to use a different service // const audio = new Audio(URL.createObjectURL(file)); // Note: This is a simplified implementation // Real file conversion would require a backend service or different approach console.log('File conversion not fully implemented in this demo'); // For demo purposes, we'll create a mock result const mockResult = { transcript: `Converted audio file: ${file.name}`, confidence: 0.95, isFinal: true, timestamp: { start: Date.now(), end: Date.now() } }; setFileResults([mockResult]); onResult?.(mockResult); } catch (error) { const errorMessage = error instanceof Error ? error.message : 'Failed to convert file'; onError?.(errorMessage); } }; const handleFileError = (error) => { onError?.(error); }; return (jsx(VoiceToTextProvider, { options: { defaultRecognitionConfig: { language: defaultLanguage, continuous: true, interimResults: true } }, onResult: onResult, onError: onError, onStart: onStart, onStop: onStop, children: jsxs("div", { className: classNames('voice-to-text-converter', className), children: [jsxs("div", { className: "voice-to-text-converter__header", children: [jsx("h1", { children: "\uD83C\uDFA4 Voice-to-Text Converter" }), jsx("p", { children: "Convert speech to text in real-time or from audio files" })] }), jsxs("div", { className: "voice-to-text-converter__content", children: [showFileUpload && showVoiceRecorder && (jsxs("div", { className: "voice-to-text-converter__tabs", children: [jsx("button", { onClick: () => setActiveTab('recorder'), className: classNames('voice-to-text-converter__tab', { 'voice-to-text-converter__tab--active': activeTab === 'recorder' }), children: "\uD83C\uDF99\uFE0F Voice Recorder" }), jsx("button", { onClick: () => setActiveTab('upload'), className: classNames('voice-to-text-converter__tab', { 'voice-to-text-converter__tab--active': activeTab === 'upload' }), children: "\uD83D\uDCC1 File Upload" })] })), jsxs("div", { className: "voice-to-text-converter__main", children: [showVoiceRecorder && (activeTab === 'recorder' || !showFileUpload) && (jsx(VoiceRecorder, { showResults: false, onResult: onResult, onError: onError, onStart: onStart, onStop: onStop })), showFileUpload && (activeTab === 'upload' || !showVoiceRecorder) && (jsx(FileUpload, { onConvert: handleFileConvert, onError: handleFileError, language: defaultLanguage }))] }), showResults && (jsx("div", { className: "voice-to-text-converter__results", children: jsx(ResultsDisplay, { results: fileResults, showClearButton: true, onClear: () => setFileResults([]) }) })), children && (jsx("div", { className: "voice-to-text-converter__children", children: children }))] })] }) })); }; export { FileUpload, LanguageSelector, RecordingControls, ResultsDisplay, VoiceRecorder, VoiceToTextConverter, VoiceToTextProvider, useVoiceToTextContext }; //# sourceMappingURL=components.esm.js.map