import express from 'express'; import cors from 'cors'; import { ControlCommand, ControlResponse, ControlServerConfig } from '../types/control.js'; import { AudioBufferManager } from '../audio/buffer-manager.js'; import { VoiceSettings } from '../types/audio.js'; export class ControlServer { private app: express.Express; private server: any; private bufferManager?: AudioBufferManager; private config: ControlServerConfig; private currentVoiceSettings?: VoiceSettings; constructor(config: Partial = {}) { this.config = { port: 3456, enableCors: true, logRequests: true, maxConnections: 10, ...config }; this.app = express(); this.setupMiddleware(); this.setupRoutes(); } private setupMiddleware(): void { if (this.config.enableCors) { this.app.use(cors({ origin: ['http://localhost:3000', 'http://127.0.0.1:3000'], credentials: true })); } this.app.use(express.json()); if (this.config.logRequests) { this.app.use((req, res, next) => { console.log(`🌐 ${req.method} ${req.path}`, req.body ? JSON.stringify(req.body) : ''); next(); }); } } private setupRoutes(): void { // Health check this.app.get('/health', (req, res) => { res.json({ status: 'ok', timestamp: new Date().toISOString(), uptime: process.uptime() }); }); // Get current playback status this.app.get('/status', (req, res) => { if (!this.bufferManager) { return res.json(this.createResponse(false, 'get_status', undefined, 'No active session')); } const progress = this.bufferManager.getPlaybackProgress(); return res.json(this.createResponse(true, 'get_status', progress)); }); // Get segments summary this.app.get('/segments', (req, res) => { if (!this.bufferManager) { return res.json(this.createResponse(false, 'get_segments', undefined, 'No active session')); } const segments = this.bufferManager.getSegmentsSummary(); return res.json(this.createResponse(true, 'get_segments', segments)); }); // Control commands this.app.post('/control', async (req, res) => { try { const command: ControlCommand = req.body; const response = await this.handleControlCommand(command); res.json(response); } catch (error) { console.error('❌ Control command error:', error); res.status(400).json(this.createResponse( false, 'error', undefined, error instanceof Error ? error.message : 'Unknown error' )); } }); // Convenience routes for common commands this.app.post('/pause', (req, res) => { const response = this.handleControlCommand({ type: 'pause', timestamp: Date.now() }); res.json(response); }); this.app.post('/resume', (req, res) => { const response = this.handleControlCommand({ type: 'resume', timestamp: Date.now() }); res.json(response); }); this.app.post('/stop', (req, res) => { const response = this.handleControlCommand({ type: 'stop', timestamp: Date.now() }); res.json(response); }); this.app.post('/rewind', (req, res) => { const response = this.handleControlCommand({ type: 'rewind', timestamp: Date.now() }); res.json(response); }); this.app.post('/skip', (req, res) => { const response = this.handleControlCommand({ type: 'skip', timestamp: Date.now() }); res.json(response); }); this.app.post('/replay', (req, res) => { const response = this.handleControlCommand({ type: 'replay', timestamp: Date.now() }); res.json(response); }); this.app.post('/speed', (req, res) => { const { speed } = req.body; if (!speed || speed < 0.5 || speed > 2.0) { return res.status(400).json(this.createResponse( false, 'speed_change', undefined, 'Speed must be between 0.5 and 2.0' )); } const response = this.handleControlCommand({ type: 'speed', payload: { speed }, timestamp: Date.now() }); return res.json(response); }); this.app.post('/seek', (req, res) => { const { chunkIndex, position } = req.body; if (chunkIndex === undefined || chunkIndex < 0) { return res.status(400).json(this.createResponse( false, 'seek', undefined, 'Valid chunkIndex is required' )); } const response = this.handleControlCommand({ type: 'seek', payload: { chunkIndex, position }, timestamp: Date.now() }); return res.json(response); }); // Error handling this.app.use((error: Error, req: express.Request, res: express.Response, next: express.NextFunction) => { console.error('🚨 Server error:', error); res.status(500).json(this.createResponse(false, 'server_error', undefined, error.message)); }); } private async handleControlCommand(command: ControlCommand): Promise { if (!this.bufferManager) { return this.createResponse(false, command.type, undefined, 'No active session'); } try { switch (command.type) { case 'pause': this.bufferManager.pause(); return this.createResponse(true, 'pause', { isPaused: true }); case 'resume': this.bufferManager.resume(); return this.createResponse(true, 'resume', { isPaused: false }); case 'stop': this.bufferManager.stop(); return this.createResponse(true, 'stop', { isStopped: true }); case 'rewind': const rewindSegment = this.bufferManager.goBack(); return this.createResponse(true, 'rewind', { segment: rewindSegment, currentIndex: this.bufferManager.getPlaybackProgress().currentChunk }); case 'skip': const skipSegment = this.bufferManager.skipForward(); return this.createResponse(true, 'skip', { segment: skipSegment, currentIndex: this.bufferManager.getPlaybackProgress().currentChunk }); case 'replay': const replaySegment = this.bufferManager.replayCurrentChunk(); return this.createResponse(true, 'replay', { segment: replaySegment }); case 'speed': if (!command.payload?.speed) { return this.createResponse(false, 'speed', undefined, 'Speed value required'); } if (this.currentVoiceSettings) { this.currentVoiceSettings.speed = command.payload.speed; } return this.createResponse(true, 'speed', { speed: command.payload.speed, message: 'Speed will apply to next chunks' }); case 'seek': if (!command.payload?.chunkIndex !== undefined) { return this.createResponse(false, 'seek', undefined, 'Chunk index required'); } const seekSegment = this.bufferManager.seekToChunk(command.payload.chunkIndex); return this.createResponse(true, 'seek', { segment: seekSegment, chunkIndex: command.payload.chunkIndex }); default: return this.createResponse(false, 'unknown_command', undefined, `Unknown command: ${command.type}`); } } catch (error) { console.error(`❌ Error executing ${command.type}:`, error); return this.createResponse( false, command.type, undefined, error instanceof Error ? error.message : 'Command execution failed' ); } } private createResponse( success: boolean, action: string, data?: any, error?: string ): ControlResponse { const response: ControlResponse = { success, action, timestamp: Date.now() }; if (data !== undefined) { response.data = data; } if (error !== undefined) { response.error = error; } return response; } setBufferManager(bufferManager: AudioBufferManager): void { this.bufferManager = bufferManager; console.log('🔗 Buffer manager connected to control server'); } setVoiceSettings(settings: VoiceSettings): void { this.currentVoiceSettings = { ...settings }; console.log('🎛️ Voice settings updated in control server'); } start(): Promise { return new Promise((resolve, reject) => { try { this.server = this.app.listen(this.config.port, () => { console.log(`🎮 JARVIS Control Server running on http://localhost:${this.config.port}`); console.log(`📡 Available endpoints:`); console.log(` GET /health - Health check`); console.log(` GET /status - Playback status`); console.log(` GET /segments - Segments summary`); console.log(` POST /control - Send control commands`); console.log(` POST /pause, /resume, /stop - Quick actions`); console.log(` POST /rewind, /skip, /replay - Navigation`); console.log(` POST /speed, /seek - Advanced controls`); resolve(); }); this.server.on('error', (error: Error) => { console.error('❌ Control server error:', error); reject(error); }); if (this.config.maxConnections) { this.server.maxConnections = this.config.maxConnections; } } catch (error) { console.error('❌ Failed to start control server:', error); reject(error); } }); } stop(): Promise { return new Promise((resolve) => { if (this.server) { this.server.close(() => { console.log('🛑 Control server stopped'); resolve(); }); } else { resolve(); } }); } isRunning(): boolean { return !!this.server?.listening; } getPort(): number { return this.config.port; } getConfig(): ControlServerConfig { return { ...this.config }; } }