import { TextChunker } from './processors/text-chunker.js'; import { ElevenLabsClient } from './audio/elevenlabs-client.js'; import { AudioBufferManager } from './audio/buffer-manager.js'; import { ControlServer } from './control/http-server.js'; import { VoiceSettings, PlaybackState, ChunkingOptions } from './types/audio.js'; import { ControlServerConfig } from './types/control.js'; export interface JarvisConfig { apiKey?: string; voice?: string; speed?: number; chunking?: Partial; controlServer?: Partial; autoStartServer?: boolean; } export class JarvisController { private textChunker: TextChunker; private elevenLabsClient: ElevenLabsClient; private bufferManager: AudioBufferManager; private controlServer: ControlServer; private currentVoiceSettings: VoiceSettings; private isInitialized: boolean = false; constructor(config: JarvisConfig = {}) { // Initialize voice settings this.currentVoiceSettings = { voice: config.voice || 'george', speed: config.speed || 1.0, stability: 0.5, similarityBoost: 0.75, style: 0.0, useSpeakerBoost: true }; // Initialize components this.textChunker = new TextChunker(); this.elevenLabsClient = new ElevenLabsClient(config.apiKey); this.bufferManager = new AudioBufferManager(this.elevenLabsClient); this.controlServer = new ControlServer(config.controlServer); // Connect components this.controlServer.setBufferManager(this.bufferManager); this.controlServer.setVoiceSettings(this.currentVoiceSettings); console.log('πŸ€– JARVIS Controller initialized'); console.log(`πŸŽ™οΈ Voice: ${this.currentVoiceSettings.voice}`); console.log(`⚑ Speed: ${this.currentVoiceSettings.speed}x`); // Auto-start control server if requested if (config.autoStartServer !== false) { this.startControlServer().catch(error => { console.error('❌ Failed to auto-start control server:', error); }); } } async startControlServer(): Promise { if (!this.controlServer.isRunning()) { await this.controlServer.start(); this.isInitialized = true; } } async stopControlServer(): Promise { if (this.controlServer.isRunning()) { await this.controlServer.stop(); } } async speakText( text: string, options?: { voice?: string; speed?: number; chunking?: Partial; waitForCompletion?: boolean; } ): Promise { console.log(`πŸ—£οΈ JARVIS speaking: "${text.substring(0, 100)}${text.length > 100 ? '...' : ''}"`); // Update voice settings if provided if (options?.voice) { this.currentVoiceSettings.voice = options.voice; } if (options?.speed) { this.currentVoiceSettings.speed = options.speed; } // Update control server with new settings this.controlServer.setVoiceSettings(this.currentVoiceSettings); try { // Chunk the text const chunks = this.textChunker.chunkText(text, options?.chunking); console.log(`πŸ“ Text chunked into ${chunks.length} segments`); // Load chunks into buffer manager await this.bufferManager.loadChunks(chunks, this.currentVoiceSettings); // Start playback by getting first segment const firstSegment = await this.bufferManager.getNextSegment(this.currentVoiceSettings); if (!firstSegment) { throw new Error('Failed to load first audio segment'); } console.log('▢️ Playback started'); // If waiting for completion, simulate playback if (options?.waitForCompletion) { await this.waitForPlaybackCompletion(); } return this.getPlaybackState(); } catch (error) { console.error('❌ Speech synthesis failed:', error); throw error; } } private async waitForPlaybackCompletion(): Promise { return new Promise((resolve) => { const checkCompletion = () => { const progress = this.bufferManager.getPlaybackProgress(); if (!progress.isPlaying && progress.currentChunk >= progress.totalChunks - 1) { console.log('🏁 Playback completed'); resolve(); } else { setTimeout(checkCompletion, 1000); // Check every second } }; checkCompletion(); }); } async getNextSegment(): Promise<{ audioBuffer?: ArrayBuffer; duration?: number; isComplete: boolean }> { const segment = await this.bufferManager.getNextSegment(this.currentVoiceSettings); if (!segment) { return { isComplete: true }; } // Mark segment as played for next call this.bufferManager.markSegmentPlayed(); const result: { audioBuffer?: ArrayBuffer; duration?: number; isComplete: boolean } = { isComplete: !this.bufferManager.hasMoreSegments() }; if (segment.audioBuffer !== undefined) { result.audioBuffer = segment.audioBuffer; } if (segment.duration !== undefined) { result.duration = segment.duration; } return result; } getPlaybackState(): PlaybackState { const progress = this.bufferManager.getPlaybackProgress(); const segments = this.bufferManager.getSegmentsSummary(); return { status: progress.isPlaying ? 'playing' : progress.isPaused ? 'paused' : 'idle', currentChunk: progress.currentChunk, totalChunks: progress.totalChunks, currentTime: 0, // TODO: Implement precise timing totalTime: this.bufferManager.getTotalDuration(), speed: this.currentVoiceSettings.speed, isPlaying: progress.isPlaying, isPaused: progress.isPaused, segments: segments.map(s => { const segment: { id: string; text: string; status: string; duration?: number } = { id: s.id, text: s.text, status: s.status }; if (s.duration !== undefined) { segment.duration = s.duration; } return segment; }) }; } pause(): void { this.bufferManager.pause(); console.log('⏸️ Playback paused'); } resume(): void { this.bufferManager.resume(); console.log('▢️ Playback resumed'); } stop(): void { this.bufferManager.stop(); console.log('⏹️ Playback stopped'); } rewind(): boolean { const segment = this.bufferManager.goBack(); if (segment) { console.log('βͺ Rewound to previous segment'); return true; } return false; } skipForward(): boolean { const segment = this.bufferManager.skipForward(); if (segment) { console.log('⏩ Skipped to next segment'); return true; } return false; } replayCurrentSegment(): boolean { const segment = this.bufferManager.replayCurrentChunk(); if (segment) { console.log('πŸ”„ Replaying current segment'); return true; } return false; } seekToChunk(chunkIndex: number): boolean { const segment = this.bufferManager.seekToChunk(chunkIndex); if (segment) { console.log(`🎯 Seeked to chunk ${chunkIndex}`); return true; } return false; } changeSpeed(speed: number): void { if (speed < 0.5 || speed > 2.0) { throw new Error('Speed must be between 0.5 and 2.0'); } this.currentVoiceSettings.speed = speed; this.controlServer.setVoiceSettings(this.currentVoiceSettings); console.log(`⚑ Speed changed to ${speed}x`); } changeVoice(voice: string): void { if (!this.elevenLabsClient.isValidVoice(voice)) { throw new Error(`Invalid voice: ${voice}. Available voices: ${this.elevenLabsClient.getAvailableVoices().join(', ')}`); } this.currentVoiceSettings.voice = voice; this.controlServer.setVoiceSettings(this.currentVoiceSettings); console.log(`πŸŽ™οΈ Voice changed to: ${voice}`); } getVoiceSettings(): VoiceSettings { return { ...this.currentVoiceSettings }; } updateVoiceSettings(settings: Partial): void { this.currentVoiceSettings = { ...this.currentVoiceSettings, ...settings }; this.controlServer.setVoiceSettings(this.currentVoiceSettings); console.log('πŸŽ›οΈ Voice settings updated'); } getAvailableVoices(): string[] { return this.elevenLabsClient.getAvailableVoices(); } getControlServerPort(): number { return this.controlServer.getPort(); } isControlServerRunning(): boolean { return this.controlServer.isRunning(); } getProgress(): { currentChunk: number; totalChunks: number; completionPercentage: number; estimatedTimeRemaining: number; } { const progress = this.bufferManager.getPlaybackProgress(); return { currentChunk: progress.currentChunk, totalChunks: progress.totalChunks, completionPercentage: progress.completionPercentage, estimatedTimeRemaining: this.bufferManager.getEstimatedTimeRemaining() }; } getSegmentsSummary(): Array<{ id: string; order: number; text: string; status: string; duration?: number; isCurrent: boolean; }> { return this.bufferManager.getSegmentsSummary(); } cleanup(): void { console.log('🧹 Cleaning up JARVIS Controller...'); this.bufferManager.cleanup(); this.stopControlServer().catch(error => { console.error('❌ Error stopping control server during cleanup:', error); }); console.log('βœ… JARVIS Controller cleaned up'); } // Static helper methods static async createWithDefaults(apiKey?: string): Promise { const config: JarvisConfig = { voice: 'george', speed: 1.0, autoStartServer: true, controlServer: { port: 3456, enableCors: true, logRequests: true } }; if (apiKey !== undefined) { config.apiKey = apiKey; } const jarvis = new JarvisController(config); // Wait for control server to start await jarvis.startControlServer(); return jarvis; } static getDefaultVoiceSettings(): VoiceSettings { return { voice: 'george', speed: 1.0, stability: 0.5, similarityBoost: 0.75, style: 0.0, useSpeakerBoost: true }; } }