@lakshmiprasanth/react-voice-to-text
Version:
A modern React package for voice-to-text conversion with real-time speech recognition and file upload support
2,160 lines âĸ 82.7 kB
JavaScript
import { jsx, jsxs } from 'react/jsx-runtime';
import { createContext, useState, useEffect, useContext, useRef, 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 the SpeechRecognition constructor for the current browser
*/
function getSpeechRecognition() {
if (typeof window === 'undefined') {
return null;
}
return (window.SpeechRecognition ||
window.webkitSpeechRecognition ||
null);
}
/**
* Check if speech recognition is supported in the current browser
*/
function isSpeechRecognitionSupported() {
const support = getBrowserSupport();
return support.webSpeechAPI;
}
/**
* Get available audio input devices
*/
async function 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 [];
}
}
/**
* Request microphone permission
*/
async function requestMicrophonePermission() {
if (typeof navigator === 'undefined' || !navigator.mediaDevices) {
return false;
}
try {
const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
// Stop the stream immediately as we only needed permission
stream.getTracks().forEach(track => track.stop());
return true;
}
catch (error) {
console.error('Microphone permission denied:', error);
return false;
}
}
/**
* Check if the current environment is a browser
*/
function isBrowser() {
return typeof window !== 'undefined' && typeof document !== 'undefined';
}
/**
* Check if the current environment is a secure context (HTTPS)
*/
function isSecureContext() {
if (typeof window === 'undefined') {
return false;
}
return window.isSecureContext || false;
}
/**
* 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 }))] })] }) }));
};
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
};
};
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 = useCallback(async () => {
await voiceToText.startRecording({
language,
continuous,
interimResults
});
}, [voiceToText, language, continuous, interimResults]);
const stopRecording = 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
};
};
const useFileUpload = (options = {}) => {
const { acceptedFormats = ['audio/*'], maxFileSize = 50 * 1024 * 1024, // 50MB
onFileSelect, onConvert, onError } = options;
const [selectedFile, setSelectedFile] = useState(null);
const [isConverting, setIsConverting] = useState(false);
const [results, setResults] = useState([]);
const [error, setError] = useState(null);
const validateFile = useCallback((file) => {
// Validate file size
if (file.size > maxFileSize) {
return {
isValid: false,
error: `File size (${(file.size / 1024 / 1024).toFixed(2)}MB) exceeds maximum allowed size (${(maxFileSize / 1024 / 1024).toFixed(2)}MB)`
};
}
// Validate file type
const isValidType = acceptedFormats.some(format => {
if (format === 'audio/*') {
return file.type.startsWith('audio/');
}
return file.type === format;
});
if (!isValidType) {
return {
isValid: false,
error: `File type ${file.type} is not supported. Please select an audio file.`
};
}
return { isValid: true };
}, [acceptedFormats, maxFileSize]);
const selectFile = useCallback((file) => {
const validation = validateFile(file);
if (!validation.isValid) {
setError(validation.error || 'Unknown validation error');
onError?.(validation.error);
return;
}
setSelectedFile(file);
setError(null);
onFileSelect?.(file);
}, [validateFile, onFileSelect, onError]);
const convertFile = useCallback(async (language = 'en-US') => {
if (!selectedFile) {
const error = 'No file selected';
setError(error);
onError?.(error);
return;
}
setIsConverting(true);
setError(null);
try {
const result = await onConvert?.(selectedFile, language);
if (result) {
setResults(prev => [...prev, result]);
}
}
catch (err) {
const errorMessage = err instanceof Error ? err.message : 'Failed to convert file';
setError(errorMessage);
onError?.(errorMessage);
}
finally {
setIsConverting(false);
}
}, [selectedFile, onConvert, onError]);
const clearFile = useCallback(() => {
setSelectedFile(null);
}, []);
const clearResults = useCallback(() => {
setResults([]);
}, []);
const clearError = useCallback(() => {
setError(null);
}, []);
return {
selectedFile,
isConverting,
results,
error,
selectFile,
convertFile,
clearFile,
clearResults,
clearError,
validateFile
};
};
const useSpeechRecognition = (options = {}) => {
const { language = 'en-US', continuous = true, interimResults = true, maxAlternatives = 1, onResult, onError, onStart, onEnd, onAudioStart, onAudioEnd, onSoundStart, onSoundEnd, onSpeechStart, onSpeechEnd, onNoMatch, onNomatch } = options;
const [isSupported, setIsSupported] = useState(false);
const [isListening, setIsListening] = useState(false);
const [transcript, setTranscript] = useState('');
const [finalTranscript, setFinalTranscript] = useState('');
const [interimTranscript, setInterimTranscript] = useState('');
const [error, setError] = useState(null);
const recognitionRef = useRef(null);
const finalTranscriptRef = useRef('');
// Check browser support and initialize
useEffect(() => {
const support = getBrowserSupport();
setIsSupported(support.webSpeechAPI);
if (support.webSpeechAPI) {
const SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition;
recognitionRef.current = new SpeechRecognition();
const recognition = recognitionRef.current;
// Configure recognition
recognition.continuous = continuous;
recognition.interimResults = interimResults;
recognition.lang = language;
recognition.maxAlternatives = maxAlternatives;
// Set up event listeners
recognition.onresult = (event) => {
let interimTranscript = '';
let finalTranscript = '';
for (let i = event.resultIndex; i < event.results.length; i++) {
const transcript = event.results[i][0].transcript;
if (event.results[i].isFinal) {
finalTranscript += transcript;
}
else {
interimTranscript += transcript;
}
}
finalTranscriptRef.current = finalTranscript;
setFinalTranscript(finalTranscript);
setInterimTranscript(interimTranscript);
setTranscript(finalTranscript + interimTranscript);
onResult?.(event);
};
recognition.onerror = (event) => {
setError(event.error);
setIsListening(false);
onError?.(event);
};
recognition.onstart = () => {
setIsListening(true);
setError(null);
onStart?.();
};
recognition.onend = () => {
setIsListening(false);
onEnd?.();
};
recognition.onaudiostart = onAudioStart;
recognition.onaudioend = onAudioEnd;
recognition.onsoundstart = onSoundStart;
recognition.onsoundend = onSoundEnd;
recognition.onspeechstart = onSpeechStart;
recognition.onspeechend = onSpeechEnd;
recognition.onnomatch = onNomatch || onNoMatch;
}
// Cleanup
return () => {
if (recognitionRef.current) {
recognitionRef.current.abort();
}
};
}, [continuous, interimResults, language, maxAlternatives, onResult, onError, onStart, onEnd, onAudioStart, onAudioEnd, onSoundStart, onSoundEnd, onSpeechStart, onSpeechEnd, onNoMatch, onNomatch]);
const start = useCallback(() => {
if (!isSupported || !recognitionRef.current) {
setError('Speech recognition is not supported');
return;
}
try {
recognitionRef.current.start();
}
catch (err) {
setError(err instanceof Error ? err.message : 'Failed to start recognition');
}
}, [isSupported]);
const stop = useCallback(() => {
if (recognitionRef.current) {
recognitionRef.current.stop();
}
}, []);
const abort = useCallback(() => {
if (recognitionRef.current) {
recognitionRef.current.abort();
}
}, []);
const reset = useCallback(() => {
setTranscript('');
setFinalTranscript('');
setInterimTranscript('');
setError(null);
finalTranscriptRef.current = '';
}, []);
const updateConfig = useCallback((config) => {
if (recognitionRef.current) {
if (config.language !== undefined) {
recognitionRef.current.lang = config.language;
}
if (config.continuous !== undefined) {
recognitionRef.current.continuous = config.continuous;
}
if (config.interimResults !== undefined) {
recognitionRef.current.interimResults = config.interimResults;
}
if (config.maxAlternatives !== undefined) {
recognitionRef.current.maxAlternatives = config.maxAlternatives;
}
}
}, []);
return {
isSupported,
isListening,
transcript,
finalTranscript,
interimTranscript,
error,
start,
stop,
abort,
reset,
updateConfig
};
};
/**
* Supported audio file formats
*/
const SUPPORTED_AUDIO_FORMATS = ['.wav', '.mp3', '.ogg', '.flac', '.m4a'];
/**
* Check if a file is a supported audio format
*/
function isSupportedAudioFormat(filePath) {
const ext = getFileExtension(filePath).toLowerCase();
return SUPPORTED_AUDIO_FORMATS.includes(ext);
}
/**
* Get file extension from file path or name
*/
function getFileExtension(filePath) {
const lastDotIndex = filePath.lastIndexOf('.');
return lastDotIndex !== -1 ? filePath.substring(lastDotIndex) : '';
}
/**
* Validate audio file format (browser version)
*/
function validateAudioFile(file) {
if (!isSupportedAudioFormat(file.name)) {
throw new Error(`Unsupported audio format. Supported formats: ${SUPPORTED_AUDIO_FORMATS.join(', ')}`);
}
}
function getAudioFileInfo(file) {
validateAudioFile(file);
const format = getFileExtension(file.name).toLowerCase().substring(1);
return {
size: file.size,
format,
name: file.name,
type: file.type,
lastModified: file.lastModified
};
}
/**
* Convert audio buffer to WAV format (simplified implementation)
*/
function bufferToWav(buffer, sampleRate = 16000, channels = 1) {
const length = buffer.byteLength;
const arrayBuffer = new ArrayBuffer(44 + length);
const view = new DataView(arrayBuffer);
// WAV header
const writeString = (offset, string) => {
for (let i = 0; i < string.length; i++) {
view.setUint8(offset + i, string.charCodeAt(i));
}
};
writeString(0, 'RIFF');
view.setUint32(4, 36 + length, true);
writeString(8, 'WAVE');
writeString(12, 'fmt ');
view.setUint32(16, 16, true);
view.setUint16(20, 1, true);
view.setUint16(22, channels, true);
view.setUint32(24, sampleRate, true);
view.setUint32(28, sampleRate * channels * 2, true);
view.setUint16(32, channels * 2, true);
view.setUint16(34, 16, true);
writeString(36, 'data');
view.setUint32(40, length, true);
// Copy audio data
const uint8Array = new Uint8Array(arrayBuffer);
uint8Array.set(new Uint8Array(buffer), 44);
return arrayBuffer;
}
/**
* Normalize audio volume
*/
function normalizeAudio(buffer, targetVolume = 0.8) {
const samples = new Int16Array(buffer);
// Find peak amplitude
let peak = 0;
for (let i = 0; i < samples.length; i++) {
const abs = Math.abs(samples[i]);
if (abs > peak)
peak = abs;
}
if (peak === 0)
return buffer;
// Calculate normalization factor
const factor = (targetVolume * 32767) / peak;
// Apply normalization
for (let i = 0; i < samples.length; i++) {
samples[i] = Math.round(samples[i] * factor);
}
return samples.buffer;
}
/**
* Detect silence in audio buffer
*/
function detectSilence(buffer, threshold = 500) {
const samples = new Int16Array(buffer);
let sum = 0;
for (let i = 0; i < samples.length; i++) {
sum += Math.abs(samples[i]);
}
const average = sum / samples.length;
return average < threshold;
}
/**
* Split audio buffer into chunks
*/
function chunkAudioBuffer(buffer, chunkSize) {
const chunks = [];
const uint8Array = new Uint8Array(buffer);
for (let i = 0; i < uint8Array.length; i += chunkSize) {
const end = Math.min(i + chunkSize, uint8Array.length);
const chunk = uint8Array.slice(i, end);
chunks.push(chunk.buffer);
}
return chunks;
}
/**
* Merge multiple audio buffers
*/
function mergeAudioBuffers(buffers) {
const totalLength = buffers.reduce((sum, buffer) => sum + buffer.byteLength, 0);
const merged = new Uint8Array(totalLength);
let offset = 0;
for (const buffer of buffers) {
merged.set(new Uint8Array(buffer), offset);
offset += buffer.byteLength;
}
return merged.buffer;
}
/**
* Convert sample rate (basic implementation)
*/
function resampleAudio(buffer, fromRate, toRate) {
if (fromRate === toRate)
return buffer;
const samples = new Int16Array(buffer);
const ratio = fromRate / toRate;
const newLength = Math.floor(samples.length / ratio);
const resampled = new Int16Array(newLength);
for (let i = 0; i < newLength; i++) {
const sourceIndex = Math.floor(i * ratio);
resampled[i] = samples[sourceIndex];
}
return resampled.buffer;
}
/**
* Create audio context for web audio processing
*/
function createAudioContext() {
try {
const AudioContextClass = window.AudioContext || window.webkitAudioContext;
return new AudioContextClass();
}
catch (error) {
console.error('Failed to create AudioContext:', error);
return null;
}
}
/**
* Load audio file as ArrayBuffer
*/
function loadAudioFile(file) {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = () => {
if (reader.result instanceof ArrayBuffer) {
resolve(reader.result);
}
else {
reject(new Error('Failed to read file as ArrayBuffer'));
}
};
reader.onerror = () => reject(reader.error);
reader.readAsArrayBuffer(file);
});
}
// Recognition utility functions for voice-to-text package
/**
* Create a new SpeechRecognition instance with the given configuration
*/
function createSpeechRecognition(config) {
const SpeechRecognition = getSpeechRecognition();
if (!SpeechRecognition) {
return null;
}
const recognition = new SpeechRecognition();
// Configure recognition properties
recognition.continuous = config.continuous ?? true;
recognition.interimResults = config.interimResults ?? true;
recognition.lang = config.language ?? 'en-US';
recognition.maxAlternatives = config.maxAlternatives ?? 1;
return recognition;
}
/**
* Check if speech recognition is available in the current environment
*/
function isSpeechRecognitionAvailable() {
const support = getBrowserSupport();
return support.webSpeechAPI;
}
/**
* Validate speech recognition configuration
*/
function validateSpeechRecognitionConfig(config) {
const errors = [];
if (config.language && !isValidLanguageCode(config.language)) {
errors.push(`Invalid language code: ${config.language}`);
}
if (config.maxAlternatives && (config.maxAlternatives < 1 || config.maxAlternatives > 10)) {
errors.push('maxAlternatives must be between 1 and 10');
}
return {
isValid: errors.length === 0,
errors
};
}
/**
* Check if a language code is valid
*/
function isValidLanguageCode(languageCode) {
// Basic validation - language codes should be in format like 'en-US', 'es-ES', etc.
const languageCodePattern = /^[a-z]{2}(-[A-Z]{2})?$/;
return languageCodePattern.test(languageCode);
}
/**
* Get supported language codes
*/
function getSupportedLanguages() {
return [
'en-US', 'en-GB', 'en-AU', 'en-CA', 'en-IN', 'en-IE', 'en-NZ', 'en-ZA',
'es-ES', 'es-MX', 'es-AR', 'es-CL', 'es-CO', 'es-PE', 'es-VE', 'es-EC',
'fr-FR', 'fr-CA', 'fr-BE', 'fr-CH', 'fr-LU', 'fr-MC',
'de-DE', 'de-AT', 'de-CH', 'de-LU', 'de-LI',
'it-IT', 'it-CH',
'pt-BR', 'pt-PT',
'ru-RU', 'ru-UA', 'ru-KZ',
'ja-JP',
'ko-KR',
'zh-CN', 'zh-TW', 'zh-HK',
'ar-SA', 'ar-EG', 'ar-IL', 'ar-JO', 'ar-LB', 'ar-MA', 'ar-PS', 'ar-SY',
'hi-IN',
'th-TH',
'tr-TR',
'pl-PL',
'nl-NL', 'nl-BE',
'sv-SE',
'da-DK',
'no-NO',
'fi-FI',
'cs-CZ',
'hu-HU',
'ro-RO',
'bg-BG',
'hr-HR',
'sk-SK',
'sl-SI',
'et-EE',
'lv-LV',
'lt-LT',
'mt-MT',
'el-GR',
'he-IL',
'id-ID',
'ms-MY',
'vi-VN',
'uk-UA',
'ca-ES',
'eu-ES',
'gl-ES',
'cy-GB',
'ga-IE',
'is-IS',
'mk-MK',
'sq-AL',
'sr-RS',
'bs-BA',
'me-ME',
'mn-MN',
'ka-GE',
'hy-AM',
'az-AZ',
'uz-UZ',
'kk-KZ',
'ky-KG',
'tg-TJ',
'fa-IR',
'ur-PK',
'bn-BD',
'si-LK',
'my-MM',
'km-KH',
'lo-LA',
'ne-NP',
'gu-IN',
'pa-IN',
'te-IN',
'kn-IN',
'ml-IN',
'ta-IN',
'or-IN',
'as-IN',
'mr-IN',
'sa-IN',
'bo-CN',
'dz-BT',
'ti-ER',
'so-SO',
'sw-KE',
'rw-RW',
'lg-UG',
'am-ET',
'ha-NG',
'yo-NG',
'ig-NG',
'zu-ZA',
'af-ZA',
'xh-ZA',
'st-ZA',
'tn-ZA',
'ts-ZA',
've-ZA',
'nr-ZA',
'ss-ZA',
'sn-ZW',
'ny-MW',
'mg-MG',
'co-FR',
'br-FR',
'oc-FR',
'gv-GB',
'kw-GB',
'fo-FO',
'sm-WS',
'to-TO',
'fj-FJ',
'haw-US',
'mi-NZ',
'qu-PE',
'qu-EC',
'qu-BO',
'ay-BO',
'gn-PY',
'gn-AR',
'gn-BO',
'gn-PE',
'gn-EC',
'gn-CO',
'gn-VE',
'gn-GY',
'gn-SR',
'gn-FR',
'gn-BR',
'gn-UY',
'gn-AR',
'gn-PY',
'gn-BO',
'gn-PE',
'gn-EC',
'gn-CO',
'gn-VE',
'gn-GY',
'gn-SR',
'gn-FR',
'gn-BR',
'gn-UY'
];
}
/**
* Get language name from language code
*/
function getLanguageName(languageCode) {
const languageNames = {
'en-US': 'English (US)',
'en-GB': 'English (UK)',
'en-AU': 'English (Australia)',
'en-CA': 'English (Canada)',
'en-IN': 'English (India)',
'en-IE': 'English (Ireland)',
'en-NZ': 'English (New Zealand)',
'en-ZA': 'English (South Africa)',
'es-ES': 'Spanish (Spain)',
'es-MX': 'Spanish (Mexico)',
'es-AR': 'Spanish (Argentina)',
'es-CL': 'Spanish (Chile)',
'es-CO': 'Spanish (Colombia)',
'es-PE': 'Spanish (Peru)',
'es-VE': 'Spanish (Venezuela)',
'es-EC': 'Spanish (Ecuador)',
'fr-FR': 'French (France)',
'fr-CA': 'French (Canada)',
'fr-BE': 'French (Belgium)',
'fr-CH': 'French (Switzerland)',
'fr-LU': 'French (Luxembourg)',
'fr-MC': 'French (Monaco)',
'de-DE': 'German (Germany)',
'de-AT': 'German (Austria)',
'de-CH': 'German (Switzerland)',
'de-LU': 'German (Luxembourg)',
'de-LI': 'German (Liechtenstein)',
'it-IT': 'Italian (Italy)',
'it-CH': 'Italian (Switzerland)',
'pt-BR': 'Portuguese (Brazil)',
'pt-PT': 'Portuguese (Portugal)',
'ru-RU': 'Russian (Russia)',
'ru-UA': 'Russian (Ukraine)',
'ru-KZ': 'Russian (Kazakhstan)',
'ja-JP': 'Japanese (Japan)',
'ko-KR': 'Korean (South Korea)',
'zh-CN': 'Chinese (Simplified)',
'zh-TW': 'Chinese (Traditional)',
'zh-HK': 'Chinese (Hong Kong)',
'ar-SA': 'Arabic (Saudi Arabia)',
'ar-EG': 'Arabic (Egypt)',
'ar-IL': 'Arabic (Israel)',
'ar-JO': 'Arabic (Jordan)',
'ar-LB': 'Arabic (Lebanon)',
'ar-MA': 'Arabic (Morocco)',
'ar-PS': 'Arabic (Palestine)',
'ar-SY': 'Arabic (Syria)',
'hi-IN': 'Hindi (India)',
'th-TH': 'Thai (Thailand)',
'tr-TR': 'Turkish (Turkey)',
'pl-PL': 'Polish (Poland)',
'nl-NL': 'Dutch (Netherlands)',
'nl-BE': 'Dutch (Belgium)',
'sv-SE': 'Swedish (Sweden)',
'da-DK': 'Danish (Denmark)',
'no-NO': 'Norwegian (Norway)',
'fi-FI': 'Finnish (Finland)',
'cs-CZ': 'Czech (Czech Republic)',
'hu-HU': 'Hungarian (Hungary)',
'ro-RO': 'Romanian (Romania)',
'bg-BG': 'Bulgarian (Bulgaria)',
'hr-HR': 'Croatian (Croatia)',
'sk-SK': 'Slovak (Slovakia)',
'sl-SI': 'Slovenian (Slovenia)',
'et-EE': 'Estonian (Estonia)',
'lv-LV': 'Latvian (Latvia)',
'lt-LT': 'Lithuanian (Lithuania)',
'mt-MT': 'Maltese (Malta)',
'el-GR': 'Greek (Greece)',
'he-IL': 'Hebrew (Israel)',
'id-ID': 'Indonesian (Indonesia)',
'ms-MY': 'Malay (Malaysia)',
'vi-VN': 'Vietnamese (Vietnam)',
'uk-UA': 'Ukrainian (Ukraine)',
'ca-ES': 'Catalan (Spain)',
'eu-ES': 'Basque (Spain)',
'gl-ES': 'Galician (Spain)',
'cy-GB': 'Welsh (UK)',
'ga-IE': 'Irish (Ireland)',
'is-IS': 'Icelandic (Iceland)',
'mk-MK': 'Macedonian (North Macedonia)',
'sq-AL': 'Albanian (Albania)',
'sr-RS': 'Serbian (Serbia)',
'bs-BA': 'Bosnian (Bosnia and Herzegovina)',
'me-ME': 'Montenegrin (Montenegro)',
'mn-MN': 'Mongolian (Mongolia)',
'ka-GE': 'Georgian (Georgia)',
'hy-AM': 'Armenian (Armenia)',
'az-AZ': 'Azerbaijani (Azerbaijan)',
'uz-UZ': 'Uzbek (Uzbekistan)',
'kk-KZ': 'Kazakh (Kazakhstan)',
'ky-KG': 'Kyrgyz (Kyrgyzstan)',
'tg-TJ': 'Tajik (Tajikistan)',
'fa-IR': 'Persian (Iran)',
'ur-PK': 'Urdu (Pakistan)',
'bn-BD': 'Bengali (Bangladesh)',
'si-LK': 'Sinhala (Sri Lanka)',
'my-MM': 'Burmese (Myanmar)',
'km-KH': 'Khmer (Cambodia)',
'lo-LA': 'Lao (Laos)',
'ne-NP': 'Nepali (Nepal)',
'gu-IN': 'Gujarati (India)',
'pa-IN': 'Punjabi (India)',
'te-IN': 'Telugu (India)',
'kn-IN': 'Kannada (India)',
'ml-IN': 'Malayalam (India)',
'ta-IN': 'Tamil (India)',
'or-IN': 'Odia (India)',
'as-IN': 'Assamese (India)',
'mr-IN': 'Marathi (India)',
'sa-IN': 'Sanskrit (India)',
'bo-CN': 'Tibetan (China)',
'dz-BT': 'Dzongkha (Bhutan)',
'ti-ER': 'Tigrinya (Eritrea)',
'so-SO': 'Somali (Somalia)',
'sw-KE': 'Swahili (Kenya)',
'rw-RW': 'Kinyarwanda (Rwanda)',
'lg-UG': 'Ganda (Uganda)',
'am-ET': 'Amharic (Ethiopia)',
'ha-NG': 'Hausa (Nigeria)',
'yo-NG': 'Yoruba (Nigeria)',
'ig-NG': 'Igbo (Nigeria)',
'zu-ZA': 'Zulu (South Africa)',
'af-ZA': 'Afrikaans (South Africa)',
'xh-ZA': 'Xhosa (South Africa)',
'st-ZA': 'Southern Sotho (South Africa)',
'tn-ZA': 'Tswana (South Africa)',
'ts-ZA': 'Tsonga (South Africa)',
've-ZA': 'Venda (South Africa)',
'nr-ZA': 'Southern Ndebele (South Africa)',
'ss-ZA': 'Swati (South Africa)',
'sn-ZW': 'Shona (Zimbabwe)',
'ny-MW': 'Chichewa (Malawi)',
'mg-MG': 'Malagasy (Madagascar)',
'co-FR': 'Corsican (France)',
'br-FR': 'Breton (France)',
'oc-FR': 'Occitan (France)',
'gv-GB': 'Manx (UK)',
'kw-GB': 'Cornish (UK)',
'fo-FO': 'Faroese (Faroe Islands)',
'sm-WS': 'Samoan (Samoa)',
'to-TO': 'Tongan (Tonga)',
'fj-FJ': 'Fijian (Fiji)',
'haw-US': 'Hawaiian (US)',
'mi-NZ': 'Maori (New Zealand)',
'qu-PE': 'Quechua (Peru)',
'qu-EC': 'Quechua (Ecuador)',
'qu-BO': 'Quechua (Bolivia)',
'ay-BO': 'Aymara (Bolivia)',
'gn-PY': 'Guarani (Paraguay)',
'gn-AR': 'Guarani (Argentina)',
'gn-BO': 'Guarani (Bolivia)',
'gn-PE': 'Guarani (Peru)',
'gn-EC': 'Guarani (Ecuador)',
'gn-CO': 'Guarani (Colombia)',
'gn-VE': 'Guarani (Venezuela)',
'gn-GY': 'Guarani (Guyana)',
'gn-SR': 'Guarani (Suriname)',
'gn-FR': 'Guarani (French Guiana)',
'gn-BR': 'Guarani (Brazil)',
'gn-UY': 'Guarani (Uruguay)'
};
return languageNames[languageCode] || languageCode;
}
/**
* Format a SpeechRecognitionResult for display
*/
function formatRecognitionResult(result) {
const confidence = result.confidence ? ` (${(result.confidence * 100).toFixed(1)}%)` : '';
const status = result.isFinal ? 'Final' : 'Interim';
return `${result.transcript}${confidence} [${status}]`;
}
/**
* Merge multiple recognition results
*/
function mergeRecognitionResults(results) {
return results
.filter(result => result.isFinal)
.map(result => result.transcript)
.join(' ');
}
/**
* Get the best result from multiple alternatives
*/
function getBestResult(results) {
if (results.length === 0) {
return null;
}
// Return the result with the highest confidence
return results.reduce((best, current) => {
if (!best)
return current;
if (!current)
return best;
return (current.confidence || 0) > (best.confidence || 0) ? current : best;
});
}
// React Voice-to-Text Package - Main Entry Point
// This package provides React components and hooks for voice-to-text functionality
// Core React components
// Version information
const VERSION = '1.0.0';
export { EventEmitter, FileUpload, LanguageSelector, RecordingControls, ResultsDisplay, SUPPORTED_AUDIO_FORMATS, SpeechRecognitionError, SpeechRecognitionErrorType, VERSION, VoiceRecorder, VoiceToTextConverter, VoiceToTextProvider, WebSpeechEngine, bufferToWav, chunkAudioBuffer, createAudioContext, createSpeechRecognition, VoiceToTextProvider as default, detectSilence, formatRecognitionResult, getAudioDevices, getAudioFileInfo, getBestResult, getBrowserInfo, getBrowserSupport, getLanguageName, getSpeechRecognition, getSupportedLanguages, isBrowser, isSecureContext, isSpeechRecognitionAvailable, isSpeechRecognitionSupported, isSupportedAudioFormat, isValidLanguageCode, loadAudioFile, mergeAudioBuffers, mergeRecognitionResults, normalizeAudio, requestMicrophonePermission, resampleAudio, useFileUpload, useSpeechRecognition, useVoiceRecorder, useVoiceToText, validateAudioFile, validateSpeechRecognitionConfig };
//# sourceMappingURL=index.esm.js.map