// Voice Command Entry System using Web Speech API // Hands-free calculation input and navigation for environmental engineers interface VoiceCommand { command: string; parameters: Record; confidence: number; timestamp: Date; } interface VoiceResponse { success: boolean; message: string; action?: string; data?: any; suggestions?: string[]; } class VoiceCommandSystem { private recognition: SpeechRecognition | null = null; private synthesis: SpeechSynthesis; private isListening: boolean = false; private commandHistory: VoiceCommand[] = []; private readonly MAX_HISTORY = 100; // Command patterns for environmental calculations private commandPatterns = new Map([ // Water quality commands [/calculate.*water.*quality.*ph\s*(\d+\.?\d*)/i, { action: 'water_ph', param: 'ph' }], [/water.*bod\s*(\d+\.?\d*)/i, { action: 'water_bod', param: 'bod' }], [/water.*cod\s*(\d+\.?\d*)/i, { action: 'water_cod', param: 'cod' }], [/water.*tds\s*(\d+\.?\d*)/i, { action: 'water_tds', param: 'tds' }], // Air quality commands [/air.*quality.*pm.*2\.5\s*(\d+\.?\d*)/i, { action: 'air_pm25', param: 'pm25' }], [/air.*pm.*10\s*(\d+\.?\d*)/i, { action: 'air_pm10', param: 'pm10' }], [/calculate.*aqi.*pm.*2\.5\s*(\d+\.?\d*).*pm.*10\s*(\d+\.?\d*)/i, { action: 'air_aqi', param: 'both' }], // Soil analysis commands [/soil.*ph\s*(\d+\.?\d*)/i, { action: 'soil_ph', param: 'ph' }], [/soil.*nitrogen\s*(\d+\.?\d*)/i, { action: 'soil_nitrogen', param: 'nitrogen' }], [/ndvi.*red\s*(\d+\.?\d*).*nir\s*(\d+\.?\d*)/i, { action: 'ndvi_calc', param: 'bands' }], // Structural calculations [/load.*estimation.*dead\s*(\d+\.?\d*).*live\s*(\d+\.?\d*)/i, { action: 'load_calc', param: 'loads' }], [/stormwater.*area\s*(\d+\.?\d*).*rainfall\s*(\d+\.?\d*)/i, { action: 'stormwater_calc', param: 'runoff' }], // Navigation commands [/open.*project\s*(.+)/i, { action: 'navigate_project', param: 'name' }], [/create.*new.*project\s*(.+)/i, { action: 'create_project', param: 'name' }], [/show.*dashboard/i, { action: 'navigate_dashboard', param: 'none' }], [/export.*data/i, { action: 'export_data', param: 'none' }], [/generate.*report/i, { action: 'generate_report', param: 'none' }], // Help commands [/help.*water.*quality/i, { action: 'help_water', param: 'none' }], [/help.*air.*quality/i, { action: 'help_air', param: 'none' }], [/help.*soil/i, { action: 'help_soil', param: 'none' }], [/what.*can.*you.*do/i, { action: 'help_general', param: 'none' }], ]); constructor() { this.synthesis = window.speechSynthesis; this.initializeSpeechRecognition(); } private initializeSpeechRecognition(): void { try { // Check for browser support const SpeechRecognition = (window as any).SpeechRecognition || (window as any).webkitSpeechRecognition; if (!SpeechRecognition) { console.warn('Speech recognition not supported in this browser'); return; } this.recognition = new SpeechRecognition(); this.recognition.continuous = false; this.recognition.interimResults = false; this.recognition.lang = 'en-US'; this.recognition.onstart = () => { this.isListening = true; this.dispatchEvent('voicestart', { listening: true }); }; this.recognition.onend = () => { this.isListening = false; this.dispatchEvent('voiceend', { listening: false }); }; this.recognition.onresult = (event: SpeechRecognitionEvent) => { const result = event.results[0]; if (result.isFinal) { const transcript = result[0].transcript.toLowerCase().trim(); const confidence = result[0].confidence; this.processVoiceCommand(transcript, confidence); } }; this.recognition.onerror = (event: SpeechRecognitionErrorEvent) => { console.error('Speech recognition error:', event.error); this.isListening = false; this.dispatchEvent('voiceerror', { error: event.error }); }; } catch (error) { console.error('Failed to initialize speech recognition:', error); } } startListening(): Promise { return new Promise((resolve, reject) => { try { if (!this.recognition) { reject(new Error('Speech recognition not available')); return; } if (this.isListening) { resolve(true); return; } this.recognition.start(); // Timeout after 10 seconds setTimeout(() => { if (this.isListening) { this.stopListening(); } }, 10000); resolve(true); } catch (error) { reject(error); } }); } stopListening(): void { if (this.recognition && this.isListening) { this.recognition.stop(); } } speak(text: string, rate: number = 1.0, pitch: number = 1.0): Promise { return new Promise((resolve, reject) => { try { if (!this.synthesis) { reject(new Error('Speech synthesis not available')); return; } // Cancel any ongoing speech this.synthesis.cancel(); const utterance = new SpeechSynthesisUtterance(text); utterance.rate = Math.max(0.1, Math.min(2.0, rate)); utterance.pitch = Math.max(0, Math.min(2, pitch)); utterance.volume = 0.8; utterance.onend = () => resolve(); utterance.onerror = (event) => reject(new Error(`Speech synthesis error: ${event.error}`)); this.synthesis.speak(utterance); } catch (error) { reject(error); } }); } private processVoiceCommand(transcript: string, confidence: number): void { try { const command: VoiceCommand = { command: transcript, parameters: {}, confidence, timestamp: new Date() }; // Store command in history this.commandHistory.push(command); if (this.commandHistory.length > this.MAX_HISTORY) { this.commandHistory.shift(); } // Process the command const response = this.parseCommand(transcript); // Update command with parsed parameters command.parameters = response.data || {}; // Dispatch event with command and response this.dispatchEvent('voicecommand', { command, response, confidence }); // Provide audio feedback if successful if (response.success && response.message) { this.speak(response.message).catch(console.error); } } catch (error) { console.error('Error processing voice command:', error); this.dispatchEvent('voicecommand', { command: { command: transcript, parameters: {}, confidence, timestamp: new Date() }, response: { success: false, message: 'Sorry, I could not process that command.', suggestions: ['Try saying "help" for available commands'] }, confidence }); } } private parseCommand(transcript: string): VoiceResponse { const normalizedText = transcript.toLowerCase().trim(); // Check each command pattern for (const [pattern, config] of this.commandPatterns) { const match = normalizedText.match(pattern); if (match) { return this.executeCommand(config.action, match, config.param); } } // No command matched return { success: false, message: 'Command not recognized. Try saying "help" for available commands.', suggestions: [ 'Calculate water quality pH 7.2', 'Air quality PM 2.5 45', 'Soil pH 6.8', 'Open project MyProject', 'Show dashboard' ] }; } private executeCommand(action: string, match: RegExpMatchArray, paramType: string): VoiceResponse { try { switch (action) { case 'water_ph': return this.handleWaterQualityCommand('ph', parseFloat(match[1])); case 'water_bod': return this.handleWaterQualityCommand('bod', parseFloat(match[1])); case 'water_cod': return this.handleWaterQualityCommand('cod', parseFloat(match[1])); case 'water_tds': return this.handleWaterQualityCommand('tds', parseFloat(match[1])); case 'air_pm25': return this.handleAirQualityCommand('pm25', parseFloat(match[1])); case 'air_pm10': return this.handleAirQualityCommand('pm10', parseFloat(match[1])); case 'air_aqi': return this.handleAQICommand(parseFloat(match[1]), parseFloat(match[2])); case 'soil_ph': return this.handleSoilCommand('ph', parseFloat(match[1])); case 'ndvi_calc': return this.handleNDVICommand(parseFloat(match[1]), parseFloat(match[2])); case 'load_calc': return this.handleLoadCalculation(parseFloat(match[1]), parseFloat(match[2])); case 'stormwater_calc': return this.handleStormwaterCommand(parseFloat(match[1]), parseFloat(match[2])); case 'navigate_project': return this.handleNavigationCommand('project', match[1].trim()); case 'create_project': return this.handleProjectCommand('create', match[1].trim()); case 'navigate_dashboard': return this.handleNavigationCommand('dashboard'); case 'export_data': return this.handleDataCommand('export'); case 'generate_report': return this.handleDataCommand('report'); case 'help_water': case 'help_air': case 'help_soil': case 'help_general': return this.handleHelpCommand(action); default: return { success: false, message: 'Unknown command action.', suggestions: ['Try saying "help" for available commands'] }; } } catch (error) { return { success: false, message: `Error executing command: ${error instanceof Error ? error.message : 'Unknown error'}`, suggestions: ['Please check your input values and try again'] }; } } private handleWaterQualityCommand(parameter: string, value: number): VoiceResponse { if (isNaN(value) || value < 0) { return { success: false, message: `Invalid ${parameter} value. Please provide a positive number.` }; } let result = ''; let recommendation = ''; switch (parameter) { case 'ph': if (value < 6.5) { result = 'acidic'; recommendation = 'Consider pH adjustment with lime treatment'; } else if (value > 8.5) { result = 'alkaline'; recommendation = 'Consider pH reduction with acid treatment'; } else { result = 'within normal range'; recommendation = 'pH is acceptable for most applications'; } break; case 'bod': if (value > 30) { result = 'high pollution level'; recommendation = 'Advanced biological treatment required'; } else { result = 'acceptable level'; recommendation = 'Current treatment appears adequate'; } break; case 'cod': if (value > 250) { result = 'high organic load'; recommendation = 'Advanced oxidation treatment may be needed'; } else { result = 'manageable level'; recommendation = 'Standard treatment should be sufficient'; } break; case 'tds': if (value > 500) { result = 'high dissolved solids'; recommendation = 'Consider reverse osmosis or ion exchange'; } else { result = 'acceptable level'; recommendation = 'TDS is within drinking water standards'; } break; } return { success: true, message: `Water ${parameter} of ${value} indicates ${result}. ${recommendation}`, action: 'calculation', data: { parameter, value, result, recommendation } }; } private handleAirQualityCommand(parameter: string, value: number): VoiceResponse { if (isNaN(value) || value < 0) { return { success: false, message: `Invalid ${parameter} value. Please provide a positive number.` }; } let category = ''; let recommendation = ''; if (parameter === 'pm25') { if (value <= 12) { category = 'Good'; recommendation = 'Air quality is satisfactory'; } else if (value <= 35.4) { category = 'Moderate'; recommendation = 'Acceptable for most people'; } else { category = 'Unhealthy'; recommendation = 'Consider limiting outdoor activities'; } } else if (parameter === 'pm10') { if (value <= 54) { category = 'Good'; recommendation = 'Air quality is satisfactory'; } else if (value <= 154) { category = 'Moderate'; recommendation = 'Acceptable for most people'; } else { category = 'Unhealthy'; recommendation = 'Limit outdoor exposure'; } } return { success: true, message: `${parameter.toUpperCase()} level of ${value} micrograms per cubic meter indicates ${category} air quality. ${recommendation}`, action: 'calculation', data: { parameter, value, category, recommendation } }; } private handleAQICommand(pm25: number, pm10: number): VoiceResponse { if (isNaN(pm25) || isNaN(pm10) || pm25 < 0 || pm10 < 0) { return { success: false, message: 'Invalid PM values. Please provide positive numbers for both PM 2.5 and PM 10.' }; } // Simplified AQI calculation const pm25AQI = this.calculatePMAQI(pm25, 'pm25'); const pm10AQI = this.calculatePMAQI(pm10, 'pm10'); const overallAQI = Math.max(pm25AQI, pm10AQI); let category = ''; if (overallAQI <= 50) category = 'Good'; else if (overallAQI <= 100) category = 'Moderate'; else if (overallAQI <= 150) category = 'Unhealthy for Sensitive Groups'; else category = 'Unhealthy'; return { success: true, message: `Air Quality Index is ${Math.round(overallAQI)}, which is ${category}. Primary pollutant is ${pm25AQI > pm10AQI ? 'PM 2.5' : 'PM 10'}.`, action: 'calculation', data: { pm25, pm10, aqi: overallAQI, category } }; } private handleSoilCommand(parameter: string, value: number): VoiceResponse { if (isNaN(value) || value < 0) { return { success: false, message: `Invalid soil ${parameter} value. Please provide a positive number.` }; } let result = ''; let recommendation = ''; if (parameter === 'ph') { if (value < 5.5) { result = 'highly acidic'; recommendation = 'Lime application recommended'; } else if (value < 6.5) { result = 'moderately acidic'; recommendation = 'Consider lime for most crops'; } else if (value < 7.5) { result = 'neutral'; recommendation = 'Optimal for most crops'; } else { result = 'alkaline'; recommendation = 'Monitor nutrient availability'; } } return { success: true, message: `Soil ${parameter} of ${value} indicates ${result} conditions. ${recommendation}`, action: 'calculation', data: { parameter, value, result, recommendation } }; } private handleNDVICommand(red: number, nir: number): VoiceResponse { if (isNaN(red) || isNaN(nir) || red < 0 || nir < 0) { return { success: false, message: 'Invalid band values. Please provide positive numbers for red and NIR bands.' }; } const ndvi = (nir - red) / (nir + red); let vegetation = ''; if (ndvi < 0.2) vegetation = 'sparse or no vegetation'; else if (ndvi < 0.5) vegetation = 'moderate vegetation'; else if (ndvi < 0.8) vegetation = 'dense vegetation'; else vegetation = 'very dense vegetation'; return { success: true, message: `NDVI value is ${ndvi.toFixed(3)}, indicating ${vegetation}.`, action: 'calculation', data: { red, nir, ndvi, vegetation } }; } private handleLoadCalculation(deadLoad: number, liveLoad: number): VoiceResponse { if (isNaN(deadLoad) || isNaN(liveLoad) || deadLoad < 0 || liveLoad < 0) { return { success: false, message: 'Invalid load values. Please provide positive numbers for dead and live loads.' }; } const designLoad = 1.5 * (deadLoad + liveLoad); return { success: true, message: `Design load is ${designLoad.toFixed(1)} kilonewtons based on dead load ${deadLoad} and live load ${liveLoad}.`, action: 'calculation', data: { deadLoad, liveLoad, designLoad } }; } private handleStormwaterCommand(area: number, rainfall: number): VoiceResponse { if (isNaN(area) || isNaN(rainfall) || area <= 0 || rainfall <= 0) { return { success: false, message: 'Invalid values. Please provide positive numbers for area and rainfall intensity.' }; } const runoff = 0.5 * rainfall * area; // Simplified calculation with C=0.5 return { success: true, message: `Estimated stormwater runoff is ${runoff.toFixed(1)} cubic meters per hour for area ${area} hectares and rainfall ${rainfall} millimeters per hour.`, action: 'calculation', data: { area, rainfall, runoff } }; } private handleNavigationCommand(destination: string, parameter?: string): VoiceResponse { return { success: true, message: `Navigating to ${destination}${parameter ? ` ${parameter}` : ''}.`, action: 'navigation', data: { destination, parameter } }; } private handleProjectCommand(action: string, name: string): VoiceResponse { return { success: true, message: `${action === 'create' ? 'Creating' : 'Opening'} project ${name}.`, action: 'project', data: { action, name } }; } private handleDataCommand(action: string): VoiceResponse { const actionText = action === 'export' ? 'Exporting data' : 'Generating report'; return { success: true, message: `${actionText} for current project.`, action: 'data', data: { action } }; } private handleHelpCommand(helpType: string): VoiceResponse { const helpMessages = { help_water: 'Water quality commands: Say "Calculate water quality pH" followed by a number, or "Water BOD", "Water COD", or "Water TDS" with values.', help_air: 'Air quality commands: Say "Air quality PM 2.5" or "Air PM 10" followed by values, or "Calculate AQI" with both PM values.', help_soil: 'Soil analysis commands: Say "Soil pH" followed by a value, or "NDVI red band" and "NIR band" with spectral values.', help_general: 'Available commands include water quality analysis, air quality calculations, soil testing, structural load estimation, stormwater calculations, project navigation, and data export. Say "help" followed by a topic for specific guidance.' }; return { success: true, message: helpMessages[helpType] || helpMessages.help_general, action: 'help', data: { helpType } }; } private calculatePMAQI(concentration: number, type: 'pm25' | 'pm10'): number { const breakpoints = type === 'pm25' ? [[0, 12, 0, 50], [12.1, 35.4, 51, 100], [35.5, 55.4, 101, 150]] : [[0, 54, 0, 50], [55, 154, 51, 100], [155, 254, 101, 150]]; for (const [cLow, cHigh, aqiLow, aqiHigh] of breakpoints) { if (concentration >= cLow && concentration <= cHigh) { return ((aqiHigh - aqiLow) / (cHigh - cLow)) * (concentration - cLow) + aqiLow; } } return 150; // Above all ranges } private dispatchEvent(eventName: string, detail: any): void { const event = new CustomEvent(eventName, { detail }); window.dispatchEvent(event); } // Public methods for external use getCommandHistory(): VoiceCommand[] { return [...this.commandHistory]; } clearCommandHistory(): void { this.commandHistory = []; } isSupported(): boolean { return !!(window as any).SpeechRecognition || !!(window as any).webkitSpeechRecognition; } isSpeechSynthesisSupported(): boolean { return 'speechSynthesis' in window; } getCurrentListeningState(): boolean { return this.isListening; } getAvailableVoices(): SpeechSynthesisVoice[] { return this.synthesis ? this.synthesis.getVoices() : []; } } export const voiceCommands = new VoiceCommandSystem(); export { VoiceCommand, VoiceResponse };