UNPKG

ttc-ai-client

Version:

TypeScript client sdk for TTC AI services with decorators and schema validation.

332 lines 13.7 kB
"use strict"; /** * TTC Call Functionality * Handles voice calls using hybrid speech services (Chrome native + TTC Speech Client fallback) */ Object.defineProperty(exports, "__esModule", { value: true }); exports.TTCCallManager = void 0; const speechIntegration_1 = require("./speechIntegration"); class TTCCallManager { constructor(options = {}) { this.speechIntegration = null; this.inactivityTimer = null; this.INACTIVITY_TIMEOUT = 10000; // 10 seconds // Enhanced state management for proper recording cycle this.currentTranscript = ''; this.isProcessingResponse = false; this.isPlayingAudio = false; this.options = options; this.state = { isActive: false, isListening: false, isSpeaking: false, transcript: '', lastActivity: Date.now() }; this.initializeSpeechIntegration(); } async initializeSpeechIntegration() { console.log('TTCCallManager: Initializing speech integration...'); // Validate required options before proceeding if (!this.options.conversationId || !this.options.token) { const missingFields = []; if (!this.options.conversationId) missingFields.push('conversationId'); if (!this.options.token) missingFields.push('token'); const errorMsg = `TTCCallManager requires ${missingFields.join(' and ')}: ${missingFields.join(', ')} missing`; console.error(errorMsg, 'Options:', this.options); throw new Error(errorMsg); } try { const speechConfig = { conversationId: this.options.conversationId, token: this.options.token, modelId: this.options.modelId, debug: true, serverUrl: this.options.serverUrl }; console.log('TTCCallManager: Speech config validation passed:', { conversationId: speechConfig.conversationId, hasToken: !!speechConfig.token, serverUrl: speechConfig.serverUrl }); const speechCallbacks = { onTranscript: (text, isFinal) => { console.log(`Transcript received: "${text}" (final: ${isFinal})`); if (isFinal) { // Only process final transcripts if (text.trim()) { console.log('Final transcript received, stopping recording and processing...'); // Stop recording immediately when we get final transcript this.stopRecording(); this.currentTranscript = text.trim(); this.state.transcript = text.trim(); this.state.lastActivity = Date.now(); this.isProcessingResponse = true; // Send the final transcript to the application this.options.onTranscript?.(text.trim()); this.resetInactivityTimer(); } else { console.log('Final transcript was empty, continuing to listen...'); } } else { // For interim results, just accumulate (don't send to server) this.currentTranscript = text; console.log(`Interim transcript: "${text}"`); } }, onSpeechStart: () => { console.log('Speech detection started'); this.state.isListening = true; this.state.lastActivity = Date.now(); this.currentTranscript = ''; this.options.onSpeechStart?.(); this.resetInactivityTimer(); }, onSpeechEnd: () => { console.log('Speech detection ended'); this.state.isListening = false; this.options.onSpeechEnd?.(); // Don't auto-restart here - let the response cycle handle it }, onAudioChunk: (audioBlob) => { console.log('Audio chunk received for TTS playback'); this.handleTTSAudio(audioBlob); }, onError: (error) => { console.error('Speech integration error:', error); this.options.onError?.(error); this.isProcessingResponse = false; this.isPlayingAudio = false; if (this.state.isActive) { this.endCall(); } } }; this.speechIntegration = await (0, speechIntegration_1.createSpeechIntegration)(speechConfig, speechCallbacks); console.log('Speech integration initialized successfully'); } catch (error) { console.error('Failed to initialize speech integration:', error); const errorMessage = error instanceof Error ? error.message : 'Failed to initialize speech services'; this.options.onError?.(errorMessage); throw error; // Re-throw so caller knows initialization failed } } resetInactivityTimer() { if (this.inactivityTimer) { clearTimeout(this.inactivityTimer); } this.inactivityTimer = window.setTimeout(() => { if (this.state.isActive && Date.now() - this.state.lastActivity > this.INACTIVITY_TIMEOUT) { console.log('Call ended due to inactivity'); this.endCall(); } }, this.INACTIVITY_TIMEOUT); } async startCall() { console.log('TTCCallManager: Starting call...'); if (this.state.isActive) { console.log('TTCCallManager: Call already active'); return false; } // Pre-flight checks if (!this.isSupported()) { console.error('TTCCallManager: Voice calls not supported'); this.options.onError?.('Voice calls are not supported in this browser. Please use Chrome, Firefox, Safari, or Edge.'); return false; } // Check network connectivity before starting console.log('TTCCallManager: Checking connectivity...'); const isOnline = await this.checkConnectivity(); if (!isOnline) { console.error('TTCCallManager: No internet connection'); this.options.onError?.('No internet connection detected. Voice calls require an internet connection.'); return false; } // Initialize speech integration if not already done if (!this.speechIntegration) { console.log('TTCCallManager: Speech integration not initialized, initializing now...'); await this.initializeSpeechIntegration(); } if (!this.speechIntegration || !this.speechIntegration.isAvailable()) { console.error('TTCCallManager: Speech services not available'); this.options.onError?.('Speech services are not available'); return false; } console.log('TTCCallManager: Speech integration available, continuing with call start...'); this.state.isActive = true; this.state.lastActivity = Date.now(); this.options.onCallStart?.(); // Start listening await this.startListening(); return true; } endCall() { if (!this.state.isActive) { return false; } this.state.isActive = false; this.state.isListening = false; this.state.isSpeaking = false; // Stop speech integration services if (this.speechIntegration) { this.speechIntegration.stopListening(); this.speechIntegration.stopSpeaking(); } // Clear timers if (this.inactivityTimer) { clearTimeout(this.inactivityTimer); this.inactivityTimer = null; } this.options.onCallEnd?.(); return true; } async startListening() { if (!this.speechIntegration || this.state.isSpeaking || this.isProcessingResponse || this.isPlayingAudio) { console.log('Cannot start listening - conditions not met:', { hasSpeechIntegration: !!this.speechIntegration, isSpeaking: this.state.isSpeaking, isProcessingResponse: this.isProcessingResponse, isPlayingAudio: this.isPlayingAudio }); return false; } try { console.log('Starting speech recognition...'); await this.speechIntegration.startListening(); this.state.isListening = true; return true; } catch (error) { console.error('Failed to start speech recognition:', error); this.options.onError?.('Failed to start listening'); return false; } } async stopRecording() { if (!this.speechIntegration || !this.state.isListening) { return; } try { console.log('Stopping speech recognition...'); await this.speechIntegration.stopListening(); this.state.isListening = false; } catch (error) { console.error('Failed to stop speech recognition:', error); } } async speak(text, options) { if (!this.speechIntegration) { throw new Error('Speech integration not initialized'); } console.log('Starting TTS synthesis for text:', text); this.state.isSpeaking = true; this.isPlayingAudio = true; // Update config if new options provided if (options?.token) { this.speechIntegration['config'].token = options.token; } if (options?.modelId) { this.speechIntegration['config'].modelId = options.modelId; } try { await this.speechIntegration.speak(text); } catch (error) { console.error('TTS synthesis failed:', error); this.state.isSpeaking = false; this.isPlayingAudio = false; throw error; } } handleTTSAudio(audioBlob) { console.log('Handling TTS audio chunk...'); // Play the audio const audioUrl = URL.createObjectURL(audioBlob); const audio = new Audio(audioUrl); audio.onended = () => { console.log('TTS audio playback completed'); URL.revokeObjectURL(audioUrl); this.onTTSPlaybackComplete(); }; audio.onerror = (error) => { console.error('TTS audio playback failed:', error); URL.revokeObjectURL(audioUrl); this.state.isSpeaking = false; this.isPlayingAudio = false; }; audio.play().catch(error => { console.error('Failed to play TTS audio:', error); URL.revokeObjectURL(audioUrl); this.state.isSpeaking = false; this.isPlayingAudio = false; }); } onTTSPlaybackComplete() { console.log('TTS playback completed, resetting state and resuming listening...'); this.state.isSpeaking = false; this.isPlayingAudio = false; this.isProcessingResponse = false; this.currentTranscript = ''; // Resume listening if call is still active if (this.state.isActive) { setTimeout(() => { if (this.state.isActive && !this.isProcessingResponse && !this.isPlayingAudio) { console.log('Resuming speech recognition after TTS...'); this.startListening(); } }, 500); // Small delay to ensure clean state transition } } onResponseReceived(responseText) { console.log('AI response received, starting TTS synthesis...'); if (this.state.isActive && responseText.trim()) { this.speak(responseText).catch(error => { console.error('Failed to synthesize response:', error); this.onTTSPlaybackComplete(); // Reset state on error }); } } getState() { return { ...this.state }; } isSupported() { // Check basic browser requirements const isOnline = navigator.onLine; if (!isOnline) { console.warn('Device is offline - speech features will not work'); return false; } // Check if speech integration can be initialized return true; // The speech integration will handle the actual capability checks } // Check network connectivity async checkConnectivity() { try { // Try to fetch a small resource to test connectivity const response = await fetch('https://www.google.com/favicon.ico', { method: 'HEAD', mode: 'no-cors', cache: 'no-cache' }); return true; } catch (error) { console.warn('Network connectivity check failed:', error); return false; } } destroy() { this.endCall(); if (this.speechIntegration) { this.speechIntegration.disconnect(); } } } exports.TTCCallManager = TTCCallManager; //# sourceMappingURL=call.js.map