UNPKG

@varunmhajan/hom-i-voice-ai

Version:

Voice AI utilities for home loan assistance with India-specific formatting

1,543 lines 53.7 kB
function getDefaultExportFromCjs (x) {
	return x && x.__esModule && Object.prototype.hasOwnProperty.call(x, 'default') ? x['default'] : x;
}

var eventemitter3 = {exports: {}};

(function (module) {

	var has = Object.prototype.hasOwnProperty
	  , prefix = '~';

	/**
	 * Constructor to create a storage for our `EE` objects.
	 * An `Events` instance is a plain object whose properties are event names.
	 *
	 * @constructor
	 * @private
	 */
	function Events() {}

	//
	// We try to not inherit from `Object.prototype`. In some engines creating an
	// instance in this way is faster than calling `Object.create(null)` directly.
	// If `Object.create(null)` is not supported we prefix the event names with a
	// character to make sure that the built-in object properties are not
	// overridden or used as an attack vector.
	//
	if (Object.create) {
	  Events.prototype = Object.create(null);

	  //
	  // This hack is needed because the `__proto__` property is still inherited in
	  // some old browsers like Android 4, iPhone 5.1, Opera 11 and Safari 5.
	  //
	  if (!new Events().__proto__) prefix = false;
	}

	/**
	 * Representation of a single event listener.
	 *
	 * @param {Function} fn The listener function.
	 * @param {*} context The context to invoke the listener with.
	 * @param {Boolean} [once=false] Specify if the listener is a one-time listener.
	 * @constructor
	 * @private
	 */
	function EE(fn, context, once) {
	  this.fn = fn;
	  this.context = context;
	  this.once = once || false;
	}

	/**
	 * Add a listener for a given event.
	 *
	 * @param {EventEmitter} emitter Reference to the `EventEmitter` instance.
	 * @param {(String|Symbol)} event The event name.
	 * @param {Function} fn The listener function.
	 * @param {*} context The context to invoke the listener with.
	 * @param {Boolean} once Specify if the listener is a one-time listener.
	 * @returns {EventEmitter}
	 * @private
	 */
	function addListener(emitter, event, fn, context, once) {
	  if (typeof fn !== 'function') {
	    throw new TypeError('The listener must be a function');
	  }

	  var listener = new EE(fn, context || emitter, once)
	    , evt = prefix ? prefix + event : event;

	  if (!emitter._events[evt]) emitter._events[evt] = listener, emitter._eventsCount++;
	  else if (!emitter._events[evt].fn) emitter._events[evt].push(listener);
	  else emitter._events[evt] = [emitter._events[evt], listener];

	  return emitter;
	}

	/**
	 * Clear event by name.
	 *
	 * @param {EventEmitter} emitter Reference to the `EventEmitter` instance.
	 * @param {(String|Symbol)} evt The Event name.
	 * @private
	 */
	function clearEvent(emitter, evt) {
	  if (--emitter._eventsCount === 0) emitter._events = new Events();
	  else delete emitter._events[evt];
	}

	/**
	 * Minimal `EventEmitter` interface that is molded against the Node.js
	 * `EventEmitter` interface.
	 *
	 * @constructor
	 * @public
	 */
	function EventEmitter() {
	  this._events = new Events();
	  this._eventsCount = 0;
	}

	/**
	 * Return an array listing the events for which the emitter has registered
	 * listeners.
	 *
	 * @returns {Array}
	 * @public
	 */
	EventEmitter.prototype.eventNames = function eventNames() {
	  var names = []
	    , events
	    , name;

	  if (this._eventsCount === 0) return names;

	  for (name in (events = this._events)) {
	    if (has.call(events, name)) names.push(prefix ? name.slice(1) : name);
	  }

	  if (Object.getOwnPropertySymbols) {
	    return names.concat(Object.getOwnPropertySymbols(events));
	  }

	  return names;
	};

	/**
	 * Return the listeners registered for a given event.
	 *
	 * @param {(String|Symbol)} event The event name.
	 * @returns {Array} The registered listeners.
	 * @public
	 */
	EventEmitter.prototype.listeners = function listeners(event) {
	  var evt = prefix ? prefix + event : event
	    , handlers = this._events[evt];

	  if (!handlers) return [];
	  if (handlers.fn) return [handlers.fn];

	  for (var i = 0, l = handlers.length, ee = new Array(l); i < l; i++) {
	    ee[i] = handlers[i].fn;
	  }

	  return ee;
	};

	/**
	 * Return the number of listeners listening to a given event.
	 *
	 * @param {(String|Symbol)} event The event name.
	 * @returns {Number} The number of listeners.
	 * @public
	 */
	EventEmitter.prototype.listenerCount = function listenerCount(event) {
	  var evt = prefix ? prefix + event : event
	    , listeners = this._events[evt];

	  if (!listeners) return 0;
	  if (listeners.fn) return 1;
	  return listeners.length;
	};

	/**
	 * Calls each of the listeners registered for a given event.
	 *
	 * @param {(String|Symbol)} event The event name.
	 * @returns {Boolean} `true` if the event had listeners, else `false`.
	 * @public
	 */
	EventEmitter.prototype.emit = function emit(event, a1, a2, a3, a4, a5) {
	  var evt = prefix ? prefix + event : event;

	  if (!this._events[evt]) return false;

	  var listeners = this._events[evt]
	    , len = arguments.length
	    , args
	    , i;

	  if (listeners.fn) {
	    if (listeners.once) this.removeListener(event, listeners.fn, undefined, true);

	    switch (len) {
	      case 1: return listeners.fn.call(listeners.context), true;
	      case 2: return listeners.fn.call(listeners.context, a1), true;
	      case 3: return listeners.fn.call(listeners.context, a1, a2), true;
	      case 4: return listeners.fn.call(listeners.context, a1, a2, a3), true;
	      case 5: return listeners.fn.call(listeners.context, a1, a2, a3, a4), true;
	      case 6: return listeners.fn.call(listeners.context, a1, a2, a3, a4, a5), true;
	    }

	    for (i = 1, args = new Array(len -1); i < len; i++) {
	      args[i - 1] = arguments[i];
	    }

	    listeners.fn.apply(listeners.context, args);
	  } else {
	    var length = listeners.length
	      , j;

	    for (i = 0; i < length; i++) {
	      if (listeners[i].once) this.removeListener(event, listeners[i].fn, undefined, true);

	      switch (len) {
	        case 1: listeners[i].fn.call(listeners[i].context); break;
	        case 2: listeners[i].fn.call(listeners[i].context, a1); break;
	        case 3: listeners[i].fn.call(listeners[i].context, a1, a2); break;
	        case 4: listeners[i].fn.call(listeners[i].context, a1, a2, a3); break;
	        default:
	          if (!args) for (j = 1, args = new Array(len -1); j < len; j++) {
	            args[j - 1] = arguments[j];
	          }

	          listeners[i].fn.apply(listeners[i].context, args);
	      }
	    }
	  }

	  return true;
	};

	/**
	 * Add a listener for a given event.
	 *
	 * @param {(String|Symbol)} event The event name.
	 * @param {Function} fn The listener function.
	 * @param {*} [context=this] The context to invoke the listener with.
	 * @returns {EventEmitter} `this`.
	 * @public
	 */
	EventEmitter.prototype.on = function on(event, fn, context) {
	  return addListener(this, event, fn, context, false);
	};

	/**
	 * Add a one-time listener for a given event.
	 *
	 * @param {(String|Symbol)} event The event name.
	 * @param {Function} fn The listener function.
	 * @param {*} [context=this] The context to invoke the listener with.
	 * @returns {EventEmitter} `this`.
	 * @public
	 */
	EventEmitter.prototype.once = function once(event, fn, context) {
	  return addListener(this, event, fn, context, true);
	};

	/**
	 * Remove the listeners of a given event.
	 *
	 * @param {(String|Symbol)} event The event name.
	 * @param {Function} fn Only remove the listeners that match this function.
	 * @param {*} context Only remove the listeners that have this context.
	 * @param {Boolean} once Only remove one-time listeners.
	 * @returns {EventEmitter} `this`.
	 * @public
	 */
	EventEmitter.prototype.removeListener = function removeListener(event, fn, context, once) {
	  var evt = prefix ? prefix + event : event;

	  if (!this._events[evt]) return this;
	  if (!fn) {
	    clearEvent(this, evt);
	    return this;
	  }

	  var listeners = this._events[evt];

	  if (listeners.fn) {
	    if (
	      listeners.fn === fn &&
	      (!once || listeners.once) &&
	      (!context || listeners.context === context)
	    ) {
	      clearEvent(this, evt);
	    }
	  } else {
	    for (var i = 0, events = [], length = listeners.length; i < length; i++) {
	      if (
	        listeners[i].fn !== fn ||
	        (once && !listeners[i].once) ||
	        (context && listeners[i].context !== context)
	      ) {
	        events.push(listeners[i]);
	      }
	    }

	    //
	    // Reset the array, or remove it completely if we have no more listeners.
	    //
	    if (events.length) this._events[evt] = events.length === 1 ? events[0] : events;
	    else clearEvent(this, evt);
	  }

	  return this;
	};

	/**
	 * Remove all listeners, or those of the specified event.
	 *
	 * @param {(String|Symbol)} [event] The event name.
	 * @returns {EventEmitter} `this`.
	 * @public
	 */
	EventEmitter.prototype.removeAllListeners = function removeAllListeners(event) {
	  var evt;

	  if (event) {
	    evt = prefix ? prefix + event : event;
	    if (this._events[evt]) clearEvent(this, evt);
	  } else {
	    this._events = new Events();
	    this._eventsCount = 0;
	  }

	  return this;
	};

	//
	// Alias methods names because people roll like that.
	//
	EventEmitter.prototype.off = EventEmitter.prototype.removeListener;
	EventEmitter.prototype.addListener = EventEmitter.prototype.on;

	//
	// Expose the prefix.
	//
	EventEmitter.prefixed = prefix;

	//
	// Allow `EventEmitter` to be imported as module namespace.
	//
	EventEmitter.EventEmitter = EventEmitter;

	//
	// Expose the module.
	//
	{
	  module.exports = EventEmitter;
	} 
} (eventemitter3));

var eventemitter3Exports = eventemitter3.exports;
var EventEmitter = /*@__PURE__*/getDefaultExportFromCjs(eventemitter3Exports);

/**
 * Ultra-fast Voice AI Client with minimal latency optimizations
 */
class VoiceClient extends EventEmitter {
    constructor(config) {
        super();
        this.conversationHistory = [];
        this.audioCache = new Map();
        this.isProcessing = false;
        this.requestQueue = [];
        this.processingQueue = false;
        // Performance tracking
        this.performanceMetrics = {
            averageLatency: 0,
            requestCount: 0,
            cacheHitRate: 0,
            errorRate: 0
        };
        // Enhanced API key validation
        if (!config.apiKey) {
            throw new Error('API key is required for @hom-i/voice-ai package');
        }
        if (typeof config.apiKey !== 'string' || config.apiKey.trim().length === 0) {
            throw new Error('API key must be a valid non-empty string');
        }
        // Basic format validation - API keys should be at least 16 characters
        if (config.apiKey.length < 16) {
            throw new Error('Invalid API key format. Please ensure you have a valid HOM-i Voice AI API key');
        }
        // Check for placeholder/example keys
        const invalidKeys = ['your-api-key', 'api-key', 'test-key', 'demo-key', 'placeholder'];
        if (invalidKeys.some(invalid => config.apiKey.toLowerCase().includes(invalid))) {
            throw new Error('Please replace the placeholder API key with your actual HOM-i Voice AI API key');
        }
        this.config = {
            apiKey: config.apiKey,
            baseUrl: config.baseUrl || 'https://chat.orbit.basichomeloan.com',
            language: config.language || 'en',
            voiceId: config.voiceId || '2bNrEsM0omyhLiEyOwqY', // Updated to Monika voice
            audioFormat: config.audioFormat || 'mp3',
            priority: config.priority || 'speed',
            mode: config.mode || 'full-voice',
            enableSTT: config.enableSTT ?? true,
            enableTTS: config.enableTTS ?? true,
            autoPlay: config.autoPlay ?? true,
            enableCompression: config.enableCompression ?? true,
            enableCaching: config.enableCaching ?? true,
            chunkSize: config.chunkSize || 4096,
            maxCacheSize: config.maxCacheSize || 50,
            timeout: config.timeout || 10000,
            retryAttempts: config.retryAttempts || 2,
            ultraFastMode: config.ultraFastMode ?? true,
            preloadVoices: config.preloadVoices ?? true,
            enableWebWorkers: config.enableWebWorkers ?? false, // Disabled by default for compatibility
        };
        this.initializeOptimizations();
    }
    async initializeOptimizations() {
        // Verify API key with backend
        await this.verifyApiKey();
        // Preload voice models if enabled
        if (this.config.preloadVoices && this.config.ultraFastMode) {
            this.preloadVoiceModels();
        }
        // Initialize Web Workers if enabled
        if (this.config.enableWebWorkers && typeof Worker !== 'undefined') {
            this.initializeWebWorkers();
        }
        this.emit('ready');
    }
    async verifyApiKey() {
        try {
            const response = await fetch(`${this.config.baseUrl}/api/voice/verify-key`, {
                method: 'POST',
                headers: { 'Content-Type': 'application/json' },
                body: JSON.stringify({ apiKey: this.config.apiKey }),
                signal: AbortSignal.timeout(5000) // 5 second timeout
            });
            if (!response.ok) {
                if (response.status === 401 || response.status === 403) {
                    throw new Error('Invalid API key. Please check your HOM-i Voice AI API credentials');
                }
                throw new Error(`API key verification failed: ${response.statusText}`);
            }
            const data = await response.json();
            if (!data.success) {
                throw new Error(data.error || 'API key verification failed');
            }
            this.emit('apiKeyVerified', { valid: true, key: this.config.apiKey.slice(0, 4) + '...' });
        }
        catch (error) {
            // If verification endpoint doesn't exist, log warning but continue
            if (error instanceof Error && error.message.includes('404')) {
                console.warn('@hom-i/voice-ai: API key verification endpoint not available, proceeding without verification');
                return;
            }
            // For other errors, throw to prevent initialization
            this.emit('apiKeyVerified', { valid: false, error: error instanceof Error ? error.message : 'Unknown error' });
            throw new Error(`API key verification failed: ${error instanceof Error ? error.message : 'Please check your API key'}`);
        }
    }
    async preloadVoiceModels() {
        try {
            // Pre-warm the voice API with a minimal request
            await this.textToSpeech('', {
                autoPlay: false,
                enableCaching: false
            });
        }
        catch {
            // Ignore preload errors
        }
    }
    initializeWebWorkers() {
        // TODO: Implement Web Worker for audio processing
        // This would move audio encoding/decoding off the main thread
    }
    /**
     * Send voice message with ultra-fast processing
     */
    async sendVoiceMessage(input, options = {}) {
        const startTime = performance.now();
        if (this.isProcessing && this.config.ultraFastMode) {
            // In ultra-fast mode, cancel previous request
            this.abortController?.abort();
        }
        this.abortController = new AbortController();
        this.isProcessing = true;
        this.emit('processingStarted', { type: 'voice_message' });
        try {
            const mergedConfig = { ...this.config, ...options };
            // Check cache first for text inputs
            if (typeof input === 'string' && this.config.enableCaching) {
                const cachedResponse = this.getCachedResponse(input, mergedConfig);
                if (cachedResponse) {
                    this.updatePerformanceMetrics(startTime, true);
                    this.emit('messageReceived', cachedResponse);
                    return cachedResponse;
                }
            }
            const requestBody = await this.prepareRequestBody(input, mergedConfig);
            const response = await this.makeRequest('/api/voice/simple-voice', requestBody);
            if (response.success) {
                this.conversationHistory = response.conversationHistory || [];
                // Cache the response
                if (typeof input === 'string' && this.config.enableCaching) {
                    this.cacheResponse(input, mergedConfig, response);
                }
                // Auto-play if enabled
                if (response.audio && mergedConfig.autoPlay) {
                    this.playAudio(response.audio);
                }
                this.updatePerformanceMetrics(startTime, false);
                this.emit('messageReceived', response);
                return response;
            }
            else {
                throw new Error(response.error || 'API request failed');
            }
        }
        catch (error) {
            this.handleError(error);
            throw error;
        }
        finally {
            this.isProcessing = false;
            this.emit('processingEnded', { type: 'voice_message' });
        }
    }
    /**
     * Send text message optimized for speed
     */
    async sendTextMessage(text, options = {}) {
        return this.sendVoiceMessage(text, { ...options, enableSTT: false });
    }
    /**
     * Transcribe audio only (no TTS response)
     */
    async transcribeAudio(audioInput, options = {}) {
        return this.sendVoiceMessage(audioInput, {
            ...options,
            enableTTS: false,
            mode: 'stt-only'
        });
    }
    /**
     * Convert text to speech with caching
     */
    async textToSpeech(text, options = {}) {
        const mergedConfig = { ...this.config, ...options };
        const cacheKey = this.generateCacheKey(text, mergedConfig);
        // Check cache first
        if (this.config.enableCaching && this.audioCache.has(cacheKey)) {
            const cachedAudio = this.audioCache.get(cacheKey);
            const response = {
                success: true,
                audio: cachedAudio,
                metadata: {
                    processingTime: 0,
                    endpoint: 'cache',
                    cached: true
                }
            };
            if (mergedConfig.autoPlay) {
                this.playAudio(cachedAudio);
            }
            this.emit('audioFromCache', { text, audio: cachedAudio });
            return response;
        }
        const response = await this.sendVoiceMessage(text, {
            ...options,
            mode: 'tts-only'
        });
        // Cache the result
        if (response.audio && this.config.enableCaching) {
            this.audioCache.set(cacheKey, response.audio);
            this.manageCacheSize();
        }
        return response;
    }
    /**
     * Play audio with optimized loading
     */
    async playAudio(audioData) {
        return new Promise((resolve, reject) => {
            try {
                const audioSrc = audioData.startsWith('data:')
                    ? audioData
                    : `data:audio/${this.config.audioFormat};base64,${audioData}`;
                const audio = new Audio(audioSrc);
                // Optimize audio loading
                audio.preload = 'auto';
                audio.crossOrigin = 'anonymous';
                audio.onloadstart = () => this.emit('audioLoadStart', audioData);
                audio.oncanplay = () => this.emit('audioCanPlay', audioData);
                audio.onplay = () => this.emit('audioPlayStart', audioData);
                audio.onended = () => {
                    this.emit('audioPlayEnd', audioData);
                    resolve();
                };
                audio.onerror = (error) => {
                    this.emit('audioError', error);
                    reject(error);
                };
                audio.play().catch(reject);
            }
            catch (error) {
                this.emit('audioError', error);
                reject(error);
            }
        });
    }
    // Request preparation and optimization methods
    async prepareRequestBody(input, config) {
        const requestBody = {
            apiKey: config.apiKey,
            voiceConfig: {
                language: config.language,
                voiceId: config.voiceId,
                audioFormat: config.audioFormat,
                priority: config.priority,
                mode: config.mode,
                enableSTT: config.enableSTT,
                enableTTS: config.enableTTS,
            },
            conversationHistory: this.conversationHistory
        };
        // Handle different input types
        if (input instanceof Blob || input instanceof File) {
            const base64Audio = await this.blobToBase64(input);
            requestBody.audioData = {
                audio: base64Audio,
                mimeType: input.type
            };
        }
        else if (input?.audio && input?.mimeType) {
            requestBody.audioData = input;
        }
        else if (typeof input === 'string') {
            requestBody.message = input;
        }
        return requestBody;
    }
    async makeRequest(endpoint, body) {
        const url = `${this.config.baseUrl}${endpoint}`;
        const response = await fetch(url, {
            method: 'POST',
            headers: { 'Content-Type': 'application/json' },
            body: JSON.stringify(body),
            signal: this.abortController?.signal,
            // Optimization headers
            keepalive: true,
        });
        if (!response.ok) {
            throw new Error(`HTTP ${response.status}: ${response.statusText}`);
        }
        return response.json();
    }
    // Caching and performance methods
    generateCacheKey(text, config) {
        return `${text}_${config.voiceId}_${config.language}_${config.audioFormat}`;
    }
    getCachedResponse(text, config) {
        const cacheKey = this.generateCacheKey(text, config);
        const cachedAudio = this.audioCache.get(cacheKey);
        if (cachedAudio) {
            return {
                success: true,
                message: text,
                audio: cachedAudio,
                metadata: {
                    processingTime: 0,
                    endpoint: 'cache',
                    cached: true
                }
            };
        }
        return null;
    }
    cacheResponse(text, config, response) {
        if (response.audio) {
            const cacheKey = this.generateCacheKey(text, config);
            this.audioCache.set(cacheKey, response.audio);
            this.manageCacheSize();
        }
    }
    manageCacheSize() {
        if (this.audioCache.size > this.config.maxCacheSize) {
            // Remove oldest entries (simple LRU)
            const entriesToRemove = this.audioCache.size - this.config.maxCacheSize;
            const entries = Array.from(this.audioCache.entries());
            for (let i = 0; i < entriesToRemove; i++) {
                this.audioCache.delete(entries[i][0]);
            }
        }
    }
    updatePerformanceMetrics(startTime, fromCache) {
        const processingTime = performance.now() - startTime;
        this.performanceMetrics.requestCount++;
        if (fromCache) {
            this.performanceMetrics.cacheHitRate =
                (this.performanceMetrics.cacheHitRate * (this.performanceMetrics.requestCount - 1) + 1) /
                    this.performanceMetrics.requestCount;
        }
        else {
            this.performanceMetrics.averageLatency =
                (this.performanceMetrics.averageLatency * (this.performanceMetrics.requestCount - 1) + processingTime) /
                    this.performanceMetrics.requestCount;
        }
    }
    handleError(error) {
        this.performanceMetrics.errorRate =
            (this.performanceMetrics.errorRate * this.performanceMetrics.requestCount + 1) /
                (this.performanceMetrics.requestCount + 1);
        this.emit('error', error);
    }
    // Utility methods
    async blobToBase64(blob) {
        return new Promise((resolve) => {
            const reader = new FileReader();
            reader.onloadend = () => {
                const base64 = reader.result.split(',')[1];
                resolve(base64);
            };
            reader.readAsDataURL(blob);
        });
    }
    // Public API methods
    getConversationHistory() {
        return [...this.conversationHistory];
    }
    clearConversationHistory() {
        this.conversationHistory = [];
        this.emit('conversationCleared');
    }
    clearCache() {
        this.audioCache.clear();
        this.emit('cacheCleared');
    }
    getPerformanceMetrics() {
        return { ...this.performanceMetrics };
    }
    getStatus() {
        return {
            isProcessing: this.isProcessing,
            cacheSize: this.audioCache.size,
            conversationLength: this.conversationHistory.length,
            config: { ...this.config },
            performance: this.getPerformanceMetrics()
        };
    }
    updateConfig(updates) {
        this.config = { ...this.config, ...updates };
        this.emit('configUpdated', this.config);
    }
    destroy() {
        this.abortController?.abort();
        this.clearCache();
        this.removeAllListeners();
    }
}

/**
 * Ultra-fast Voice Recorder with advanced VAD and low-latency processing
 */
class VoiceRecorder extends EventEmitter {
    constructor(config = {}) {
        super();
        this.mediaRecorder = null;
        this.mediaStream = null;
        this.audioContext = null;
        this.analyser = null;
        this.scriptProcessor = null;
        this.recordingState = {
            isRecording: false,
            isProcessing: false,
            voiceActivityDetected: false,
            audioLevel: 0,
            recordingDuration: 0,
            error: null
        };
        this.audioChunks = [];
        this.recordingStartTime = 0;
        this.silenceTimer = null;
        this.vadTimer = null;
        this.animationFrame = null;
        // Performance monitoring
        this.performanceMetrics = {
            totalRecordings: 0,
            averageRecordingDuration: 0,
            vadAccuracy: 0,
            processingLatency: 0
        };
        this.config = {
            // Audio settings
            sampleRate: config.sampleRate || 44100,
            channels: config.channels || 1,
            audioBitsPerSecond: config.audioBitsPerSecond || 128000,
            mimeType: config.mimeType || 'audio/webm;codecs=opus',
            // Voice Activity Detection
            enableVAD: config.enableVAD ?? true,
            vadThreshold: config.vadThreshold || 0.01,
            silenceTimeout: config.silenceTimeout || 1500,
            minRecordingTime: config.minRecordingTime || 500,
            maxRecordingTime: config.maxRecordingTime || 30000,
            // Performance optimizations
            chunkDuration: config.chunkDuration || 100,
            enableNoiseSuppression: config.enableNoiseSuppression ?? true,
            enableEchoCancellation: config.enableEchoCancellation ?? true,
            enableAutoGainControl: config.enableAutoGainControl ?? true,
            // Ultra-fast mode
            ultraFastMode: config.ultraFastMode ?? true,
            enableRealTimeProcessing: config.enableRealTimeProcessing ?? false,
            bufferSize: config.bufferSize || 4096,
        };
        this.initializeAudioContext();
    }
    async initializeAudioContext() {
        try {
            // Create optimized audio context
            const AudioContextClass = window.AudioContext || window.webkitAudioContext;
            this.audioContext = new AudioContextClass({
                sampleRate: this.config.sampleRate,
                latencyHint: this.config.ultraFastMode ? 'interactive' : 'balanced',
            });
            // Resume if suspended
            if (this.audioContext.state === 'suspended') {
                await this.audioContext.resume();
            }
            this.emit('initialized');
        }
        catch (error) {
            this.handleError('Failed to initialize audio context', error);
        }
    }
    /**
     * Start recording with optimized settings
     */
    async startRecording() {
        if (this.recordingState.isRecording) {
            throw new Error('Already recording');
        }
        const startTime = performance.now();
        try {
            // Get optimized media stream
            this.mediaStream = await navigator.mediaDevices.getUserMedia({
                audio: {
                    sampleRate: this.config.sampleRate,
                    channelCount: this.config.channels,
                    echoCancellation: this.config.enableEchoCancellation,
                    noiseSuppression: this.config.enableNoiseSuppression,
                    autoGainControl: this.config.enableAutoGainControl,
                    // Ultra-fast mode optimizations
                    ...(this.config.ultraFastMode && {
                        latency: 0.01, // 10ms latency
                        sampleSize: 16,
                    })
                }
            });
            // Setup audio analysis
            await this.setupAudioAnalysis();
            // Setup media recorder with optimized settings
            this.setupMediaRecorder();
            // Start recording
            this.audioChunks = [];
            this.recordingStartTime = Date.now();
            this.mediaRecorder.start(this.config.chunkDuration);
            // Update state
            this.recordingState = {
                ...this.recordingState,
                isRecording: true,
                error: null,
                recordingDuration: 0
            };
            // Start VAD if enabled
            if (this.config.enableVAD) {
                this.startVoiceActivityDetection();
            }
            // Start duration tracking
            this.startDurationTracking();
            // Track performance
            const initLatency = performance.now() - startTime;
            this.updatePerformanceMetrics('initLatency', initLatency);
            this.emit('recordingStarted', {
                timestamp: this.recordingStartTime,
                config: this.config
            });
        }
        catch (error) {
            this.handleError('Failed to start recording', error);
            throw error;
        }
    }
    /**
     * Stop recording and return audio data
     */
    async stopRecording() {
        if (!this.recordingState.isRecording) {
            throw new Error('Not currently recording');
        }
        const stopTime = performance.now();
        return new Promise((resolve, reject) => {
            if (!this.mediaRecorder) {
                reject(new Error('Media recorder not initialized'));
                return;
            }
            // Setup completion handler
            const handleStop = () => {
                const recordingDuration = Date.now() - this.recordingStartTime;
                // Combine all audio chunks
                const audioBlob = new Blob(this.audioChunks, {
                    type: this.config.mimeType
                });
                // Update performance metrics
                this.performanceMetrics.totalRecordings++;
                this.performanceMetrics.averageRecordingDuration =
                    (this.performanceMetrics.averageRecordingDuration * (this.performanceMetrics.totalRecordings - 1) + recordingDuration) /
                        this.performanceMetrics.totalRecordings;
                // Cleanup
                this.cleanup();
                // Update state
                this.recordingState = {
                    ...this.recordingState,
                    isRecording: false,
                    recordingDuration
                };
                const processingTime = performance.now() - stopTime;
                this.emit('recordingStopped', {
                    audioBlob,
                    duration: recordingDuration,
                    processingTime,
                    chunks: this.audioChunks.length
                });
                resolve(audioBlob);
            };
            // Handle data available
            this.mediaRecorder.ondataavailable = (event) => {
                if (event.data.size > 0) {
                    this.audioChunks.push(event.data);
                    // Emit chunk for real-time processing if enabled
                    if (this.config.enableRealTimeProcessing) {
                        this.emit('audioChunk', {
                            data: event.data,
                            timestamp: Date.now(),
                            duration: this.config.chunkDuration,
                            isLast: false
                        });
                    }
                }
            };
            this.mediaRecorder.onstop = handleStop;
            this.mediaRecorder.onerror = (event) => {
                this.handleError('Media recorder error', event);
                reject(new Error('Recording failed'));
            };
            // Stop recording
            this.mediaRecorder.stop();
            // Stop timers
            this.stopVoiceActivityDetection();
            this.stopDurationTracking();
        });
    }
    /**
     * Setup audio analysis for VAD and level monitoring
     */
    async setupAudioAnalysis() {
        if (!this.audioContext || !this.mediaStream) {
            throw new Error('Audio context or media stream not available');
        }
        try {
            // Create analyser for real-time audio analysis
            this.analyser = this.audioContext.createAnalyser();
            this.analyser.fftSize = this.config.bufferSize;
            this.analyser.smoothingTimeConstant = 0.1; // Faster response
            this.analyser.minDecibels = -100;
            this.analyser.maxDecibels = -10;
            // Connect media stream to analyser
            const source = this.audioContext.createMediaStreamSource(this.mediaStream);
            source.connect(this.analyser);
            // Setup script processor for real-time processing (if enabled)
            if (this.config.enableRealTimeProcessing) {
                this.scriptProcessor = this.audioContext.createScriptProcessor(this.config.bufferSize, this.config.channels, this.config.channels);
                this.scriptProcessor.onaudioprocess = (event) => {
                    const inputBuffer = event.inputBuffer.getChannelData(0);
                    this.processAudioBuffer(inputBuffer);
                };
                source.connect(this.scriptProcessor);
                this.scriptProcessor.connect(this.audioContext.destination);
            }
        }
        catch (error) {
            throw new Error(`Failed to setup audio analysis: ${error}`);
        }
    }
    /**
     * Setup optimized media recorder
     */
    setupMediaRecorder() {
        if (!this.mediaStream) {
            throw new Error('Media stream not available');
        }
        try {
            // Find the best supported MIME type
            const mimeType = this.findBestMimeType();
            this.mediaRecorder = new MediaRecorder(this.mediaStream, {
                mimeType,
                audioBitsPerSecond: this.config.audioBitsPerSecond,
            });
            // Update config with actual MIME type
            this.config.mimeType = mimeType;
        }
        catch (error) {
            throw new Error(`Failed to create media recorder: ${error}`);
        }
    }
    /**
     * Find the best supported MIME type for recording
     */
    findBestMimeType() {
        const mimeTypes = [
            'audio/webm;codecs=opus',
            'audio/webm',
            'audio/mp4',
            'audio/ogg;codecs=opus',
            'audio/ogg',
            'audio/wav'
        ];
        for (const mimeType of mimeTypes) {
            if (MediaRecorder.isTypeSupported(mimeType)) {
                return mimeType;
            }
        }
        // Fallback to any supported type
        return this.config.mimeType;
    }
    /**
     * Voice Activity Detection with optimized algorithms
     */
    startVoiceActivityDetection() {
        if (!this.analyser)
            return;
        const detectVoiceActivity = () => {
            if (!this.analyser || !this.recordingState.isRecording)
                return;
            const dataArray = new Uint8Array(this.analyser.frequencyBinCount);
            this.analyser.getByteFrequencyData(dataArray);
            // Calculate RMS (Root Mean Square) for better VAD
            const sum = dataArray.reduce((acc, value) => acc + value * value, 0);
            const rms = Math.sqrt(sum / dataArray.length);
            const normalizedLevel = rms / 255;
            // Update audio level
            this.recordingState.audioLevel = normalizedLevel;
            // Voice activity detection
            const isVoiceDetected = normalizedLevel > this.config.vadThreshold;
            const previousVAD = this.recordingState.voiceActivityDetected;
            this.recordingState.voiceActivityDetected = isVoiceDetected;
            // Emit level updates
            this.emit('audioLevel', {
                level: normalizedLevel,
                isVoiceDetected,
                timestamp: Date.now()
            });
            // Handle voice activity changes
            if (isVoiceDetected && !previousVAD) {
                this.emit('voiceStart', { timestamp: Date.now() });
                this.clearSilenceTimer();
            }
            else if (!isVoiceDetected && previousVAD) {
                this.emit('voiceEnd', { timestamp: Date.now() });
                this.startSilenceTimer();
            }
            // Continue monitoring
            this.animationFrame = requestAnimationFrame(detectVoiceActivity);
        };
        detectVoiceActivity();
    }
    /**
     * Stop voice activity detection
     */
    stopVoiceActivityDetection() {
        if (this.animationFrame) {
            cancelAnimationFrame(this.animationFrame);
            this.animationFrame = null;
        }
        this.clearSilenceTimer();
    }
    /**
     * Start silence timer for auto-stop
     */
    startSilenceTimer() {
        this.clearSilenceTimer();
        this.silenceTimer = setTimeout(async () => {
            const recordingDuration = Date.now() - this.recordingStartTime;
            // Only auto-stop if recording is long enough
            if (recordingDuration >= this.config.minRecordingTime) {
                this.emit('silenceDetected', {
                    duration: recordingDuration,
                    autoStopping: true
                });
                try {
                    await this.stopRecording();
                }
                catch (error) {
                    this.handleError('Auto-stop failed', error);
                }
            }
        }, this.config.silenceTimeout);
    }
    /**
     * Clear silence timer
     */
    clearSilenceTimer() {
        if (this.silenceTimer) {
            clearTimeout(this.silenceTimer);
            this.silenceTimer = null;
        }
    }
    /**
     * Start duration tracking with auto-stop
     */
    startDurationTracking() {
        const updateDuration = () => {
            if (!this.recordingState.isRecording)
                return;
            const duration = Date.now() - this.recordingStartTime;
            this.recordingState.recordingDuration = duration;
            // Auto-stop if max duration reached
            if (duration >= this.config.maxRecordingTime) {
                this.emit('maxDurationReached', { duration });
                this.stopRecording().catch(error => {
                    this.handleError('Auto-stop on max duration failed', error);
                });
                return;
            }
            // Continue tracking
            setTimeout(updateDuration, 100);
        };
        updateDuration();
    }
    /**
     * Stop duration tracking
     */
    stopDurationTracking() {
        // Duration tracking stops automatically when recording stops
    }
    /**
     * Process audio buffer for real-time analysis
     */
    processAudioBuffer(buffer) {
        // Calculate additional metrics
        const peak = Math.max(...buffer.map(Math.abs));
        const energy = buffer.reduce((sum, sample) => sum + sample * sample, 0) / buffer.length;
        this.emit('audioBuffer', {
            buffer: buffer.slice(), // Copy for safety
            peak,
            energy,
            timestamp: Date.now()
        });
    }
    /**
     * Update performance metrics
     */
    updatePerformanceMetrics(metric, value) {
        if (metric === 'initLatency') {
            this.performanceMetrics.processingLatency =
                (this.performanceMetrics.processingLatency + value) / 2;
        }
    }
    /**
     * Handle errors consistently
     */
    handleError(message, error) {
        const errorMessage = error instanceof Error ? error.message : String(error);
        this.recordingState.error = `${message}: ${errorMessage}`;
        console.error(`VoiceRecorder Error - ${message}:`, error);
        this.emit('error', {
            message,
            error: errorMessage,
            timestamp: Date.now()
        });
    }
    /**
     * Cleanup resources
     */
    cleanup() {
        // Stop tracks
        if (this.mediaStream) {
            this.mediaStream.getTracks().forEach(track => track.stop());
            this.mediaStream = null;
        }
        // Cleanup audio nodes
        if (this.scriptProcessor) {
            this.scriptProcessor.disconnect();
            this.scriptProcessor = null;
        }
        if (this.analyser) {
            this.analyser.disconnect();
            this.analyser = null;
        }
        // Clear timers
        this.stopVoiceActivityDetection();
        this.clearSilenceTimer();
    }
    // Public API methods
    getState() {
        return { ...this.recordingState };
    }
    getConfig() {
        return { ...this.config };
    }
    getPerformanceMetrics() {
        return { ...this.performanceMetrics };
    }
    updateConfig(updates) {
        this.config = { ...this.config, ...updates };
        this.emit('configUpdated', this.config);
    }
    /**
     * Check if browser supports recording
     */
    static isSupported() {
        return !!(typeof navigator !== 'undefined' &&
            navigator.mediaDevices &&
            typeof navigator.mediaDevices.getUserMedia === 'function' &&
            typeof MediaRecorder !== 'undefined' &&
            (window.location.protocol === 'https:' || window.location.hostname === 'localhost'));
    }
    /**
     * Get supported MIME types
     */
    static getSupportedMimeTypes() {
        const mimeTypes = [
            'audio/webm;codecs=opus',
            'audio/webm',
            'audio/mp4',
            'audio/ogg;codecs=opus',
            'audio/ogg',
            'audio/wav'
        ];
        return mimeTypes.filter(type => MediaRecorder.isTypeSupported(type));
    }
    /**
     * Destroy recorder and cleanup all resources
     */
    destroy() {
        if (this.recordingState.isRecording) {
            this.stopRecording().catch(() => {
                // Ignore stop errors during destroy
            });
        }
        this.cleanup();
        if (this.audioContext && this.audioContext.state !== 'closed') {
            this.audioContext.close();
        }
        this.removeAllListeners();
    }
}

// Core exports
// Version info
const VERSION = '1.6.0';
// Utilities
const utils = {
    /**
     * Check if voice features are supported in the current environment
     */
    isVoiceSupported() {
        return !!(typeof navigator !== 'undefined' &&
            navigator.mediaDevices &&
            typeof navigator.mediaDevices.getUserMedia === 'function' &&
            (window.location.protocol === 'https:' || window.location.hostname === 'localhost'));
    },
    /**
     * Check if recording is supported
     */
    isRecordingSupported() {
        return !!(typeof MediaRecorder !== 'undefined' &&
            this.isVoiceSupported());
    },
    /**
     * Get optimal voice configuration for different scenarios
     */
    getOptimalConfig(scenario) {
        switch (scenario) {
            case 'ultra-fast':
                return {
                    priority: 'speed',
                    ultraFastMode: true,
                    enableCaching: true,
                    enableCompression: true,
                    audioFormat: 'mp3',
                    timeout: 5000,
                    maxCacheSize: 100
                };
            case 'high-quality':
                return {
                    priority: 'quality',
                    ultraFastMode: false,
                    enableCaching: false,
                    enableCompression: false,
                    audioFormat: 'wav',
                    timeout: 30000,
                    maxCacheSize: 10
                };
            case 'balanced':
            default:
                return {
                    priority: 'balanced',
                    ultraFastMode: true,
                    enableCaching: true,
                    enableCompression: true,
                    audioFormat: 'mp3',
                    timeout: 15000,
                    maxCacheSize: 50
                };
        }
    },
    /**
     * Create a VoiceClient with optimal settings for quick setup
     */
    createQuickClient(apiKey, scenario = 'balanced') {
        const config = {
            apiKey,
            ...this.getOptimalConfig(scenario)
        };
        return new VoiceClient(config);
    },
    /**
     * Get performance recommendations based on metrics
     */
    getPerformanceRecommendations(metrics) {
        const recommendations = [];
        if (metrics.averageLatency > 2000) {
            recommendations.push('Consider enabling ultra-fast mode for better latency');
            recommendations.push('Check your network connection');
        }
        if (metrics.cacheHitRate < 0.3) {
            recommendations.push('Enable caching to improve performance');
            recommendations.push('Consider increasing cache size');
        }
        if (metrics.errorRate > 0.1) {
            recommendations.push('Check API key and network connectivity');
            recommendations.push('Consider implementing retry logic');
        }
        if (recommendations.length === 0) {
            recommendations.push('Performance looks good! 🎉');
        }
        return recommendations;
    },
    /**
     * Format text for optimal voice interaction (short and conversational)
     */
    formatForVoice(text, maxWords = 6) {
        if (!text || typeof text !== 'string')
            return '';
        // Remove common filler words and phrases for voice
        const fillerWords = ['actually', 'basically', 'essentially', 'definitely', 'certainly', 'absolutely', 'of course', 'you know', 'I think', 'I believe', 'let me', 'well,', 'so,', 'now,'];
        let words = text.split(/\s+/).filter(w => w.trim());
        // Remove filler words
        words = words.filter(word => !fillerWords.some(filler => word.toLowerCase().includes(filler.toLowerCase())));
        // Try to extract key phrases first
        const keyPhrases = [
            'yes', 'no', 'sure', 'okay', 'great', 'good', 'got it', 'checking', 'done',
            'right', 'correct', 'exactly', 'perfect', 'sounds good', 'what city',
            'your budget', 'tell me', 'which', 'how much', 'when', 'where'
        ];
        const lowerText = text.toLowerCase();
        const foundKeyPhrase = keyPhrases.find(phrase => lowerText.includes(phrase));
        let result = '';
        if (foundKeyPhrase && foundKeyPhrase.split(' ').length <= 3) {
            // Use the key phrase if it's short enough
            result = foundKeyPhrase.charAt(0).toUpperCase() + foundKeyPhrase.slice(1);
        }
        else if (words.length <= maxWords) {
            // Use cleaned words if short enough
            result = words.join(' ');
        }
        else {
            // Take first maxWords maximum
            result = words.slice(0, maxWords).join(' ');
        }
        // Clean up and add appropriate punctuation
        result = result.trim();
        // Remove unnecessary punctuation
        result = result.replace(/[.!?]+$/, '');
        // Add appropriate punctuation
        if (result.toLowerCase().match(/^(yes|sure|great|good|okay|right|correct|exactly|perfect|sounds good)$/)) {
            result += '!';
        }
        else if (result.toLowerCase().includes('?') || result.toLowerCase().match(/^(what|which|how|when|where|why)/)) {
            if (!result.includes('?')) {
                result += '?';
            }
        }
        else {
            result += '.';
        }
        return result;
    },
    /**
     * Create voice-optimized configuration for ultra-short responses
     */
    getVoiceOptimizedConfig(apiKey) {
        return {
            apiKey,
            priority: 'speed',
            ultraFastMode: true,
            enableCaching: true,
            enableCompression: true,
            audioFormat: 'mp3',
            timeout: 5000,
            maxCacheSize: 100,
            // Voice-specific optimizations
            language: 'en',
            voiceId: 'N2al4jd45e882svx17SU', // Optimized voice for speed
            autoPlay: true,
            enableSTT: true,
            enableTTS: true
        };
    }
};

// Core exports (re-export for vanilla usage)
// Vanilla JavaScript helpers for easier integration
const VanillaVoiceAI = {
    /**
     * Quick setup for vanilla JS projects
     */
    createClient(apiKey, options = {}) {
        const { VoiceClient, utils } = require('../index');
        const config = {
            apiKey,
            ...utils.getOptimalConfig(options.mode || 'balanced'),
            ...options
        };
        return new VoiceClient(config);
    },
    /**
     * Quick setup for recorder
     */
    createRecorder(options = {}) {
        const { VoiceRecorder } = require('../index');
        return new VoiceRecorder(options);
    },
    /**
     * Check browser support
     */
    checkSupport() {
        const { utils, VoiceRecorder } = require('../index');
        return {
            voiceSupported: utils.isVoiceSupported(),
            recordingSupported: utils.isRecordingSupported(),
            supportedMimeTypes: VoiceRecorder.getSupportedMimeTypes(),
            isHttps: window.location.protocol === 'https:' || window.location.hostname === 'localhost'
        };
    },
    /**
     * Simple text-to-speech function
     */
    async speak(text, apiKey, options = {}) {
        const client = this.createClient(apiKey, {
            ...options,
            mode: 'tts-only',
            autoPlay: true
        });
        try {
            const response = await client.textToSpeech(text);
            return response;
        }
        finally {
            client.destroy();
        }
    },
    /**
     * Simple speech-to-text function
     */
    async transcribe(audioBlob, apiKey, options = {}) {
        const client = this.createClient(apiKey, {
            ...options,
            mode: 'stt-only'
        });
        try {
            const response = await client.transcribeAudio(audioBlob);
            return response;
        }
        finally {
            client.destroy();
        }
    },
    /**
     * Record and transcribe in one function
     */
    async recordAndTranscribe(apiKey, recorderOptions = {}, clientOptions = {}) {
        const recorder = this.createRecorder({
            enableVAD: true,
            silenceTimeout: 1500,
            ...recorderOptions
        });
        const client = this.createClient(apiKey, {
            mode: 'stt-only',
            ...clientOptions
        });
        try {
            // Start recording
            await recorder.startRecording();
            // Wait for recording to complete (via VAD or manual stop)
            return new Promise((resolve, reject) => {
                recorder.on('recordingStopped', async ({ audioBlob }) => {
                    try {
                        const response = await client.transcribeAudio(audioBlob);
                        resolve(response);
                    }
                    catch (error) {
                        reject(error);
                    }
                });
                recorder.on('error', reject);
            });
        }
        finally {
            recorder.destroy();
            client.destroy();
        }
    },
    /**
     * Full voice chat function
     */
    async voiceChat(input, apiKey, options = {}) {
        const client = this.createClient(apiKey, {
            mode: 'full-voice',
            autoPlay: true,
            ...options
        });
        try {
            const response = await client.sendVoiceMessage(input);
            return response;
        }
        finally {
            client.destroy();
        }
    }
};
// Make available globally for script tag usage
if (typeof window !== 'undefined') {
    window.HomiVoiceAI = {
        VoiceClient,
        VoiceRecorder,
        utils,
        VERSION,
        ...VanillaVoiceAI
    };
}

export { VERSION, VanillaVoiceAI, VoiceClient, VoiceRecorder, utils };
//# sourceMappingURL=index.esm.js.map