Spaces:
Configuration error
Configuration error
| // Voice Command Entry System using Web Speech API | |
| // Hands-free calculation input and navigation for environmental engineers | |
| interface VoiceCommand { | |
| command: string; | |
| parameters: Record<string, any>; | |
| 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<boolean> { | |
| 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<void> { | |
| 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 }; |