ttc-ai-client
Version:
TypeScript client sdk for TTC AI services with decorators and schema validation.
332 lines • 13.7 kB
JavaScript
"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