PrithviGuardian / client /src /lib /voiceCommands.ts
Varad Bakshi
Fresh upload
e7427b5
Raw
History Blame Contribute Delete
20.9 kB
// 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 };