/** * Voice Intent Classification * * Classifies voice commands into structured activity intents * for feeding, sleep, diaper tracking with entity extraction. */ export enum IntentType { FEEDING = 'feeding', SLEEP = 'sleep', DIAPER = 'diaper', UNKNOWN = 'unknown', } export enum FeedingType { BOTTLE = 'bottle', BREAST_LEFT = 'breast_left', BREAST_RIGHT = 'breast_right', BREAST_BOTH = 'breast_both', SOLID = 'solid', } export enum SleepType { NAP = 'nap', NIGHT = 'night', } export enum DiaperType { WET = 'wet', DIRTY = 'dirty', BOTH = 'both', DRY = 'dry', } export interface Entity { type: string; value: any; confidence: number; text: string; } export interface ClassificationResult { intent: IntentType; confidence: number; subtype?: string; entities: Entity[]; rawText: string; structuredData?: FeedingData | SleepData | DiaperData; } export interface FeedingData { type: FeedingType; amount?: number; unit?: 'ml' | 'oz' | 'tbsp'; duration?: number; // minutes side?: 'left' | 'right' | 'both'; startTime?: Date; endTime?: Date; notes?: string; } export interface SleepData { type: SleepType; startTime?: Date; endTime?: Date; duration?: number; // minutes notes?: string; } export interface DiaperData { type: DiaperType; time?: Date; notes?: string; } /** * Intent classification patterns */ const INTENT_PATTERNS = { feeding: [ // Bottle feeding /fed\s+(baby|him|her)?\s*(\d+)?\s*(ml|milliliters?|oz|ounces?|tbsp|tablespoons?)?/i, /gave\s+(baby|him|her)?\s*(\d+)?\s*(ml|milliliters?|oz|ounces?)?/i, /bottle\s*fed/i, /(bottle|formula)\s*feeding/i, /drank\s*(\d+)?\s*(ml|oz)/i, // Breastfeeding /breast\s*fed/i, /breastfe(e|a)ding/i, /nursed/i, /nursing/i, /(fed|feeding)\s+(from|on)\s+(left|right|both)\s+(breast|side)/i, // Solid food /ate\s+(solid|food|meal)/i, /solid\s+food/i, /had\s+(breakfast|lunch|dinner|snack)/i, // General feeding /feed\s+(start|begin|end)/i, /feeding\s+time/i, ], sleep: [ /sleep/i, /nap/i, /sleeping/i, /napping/i, /fell\s+asleep/i, /woke\s+up/i, /asleep/i, /bedtime/i, /put\s+(baby|him|her)\s+(down|to\s+sleep)/i, ], diaper: [ /diaper/i, /nappy/i, /changed\s+(diaper|nappy)/i, /diaper\s+change/i, /(wet|dirty|soiled|poopy|poop|pee)\s+(diaper|nappy)?/i, /bowel\s+movement/i, /bm\b/i, ], }; /** * Entity extraction patterns */ const ENTITY_PATTERNS = { // Amounts with units amount: [ /(\d+(?:\.\d+)?)\s*(ml|milliliters?|cc)/gi, /(\d+(?:\.\d+)?)\s*(oz|ounces?)/gi, /(\d+(?:\.\d+)?)\s*(tbsp|tablespoons?)/gi, ], // Duration in minutes duration: [ /(\d+)\s*(minutes?|mins?)\b/gi, /for\s+(\d+)\s*(minutes?|mins?)/gi, /lasted\s+(\d+)\s*(minutes?|mins?)/gi, ], // Time expressions time: [ /at\s+(\d{1,2}):(\d{2})\s*(am|pm)?/gi, /(\d{1,2})\s*(am|pm)/gi, /(morning|afternoon|evening|night)/gi, ], // Relative time relativeTime: [ /(\d+)\s*(hours?|hrs?)\s+ago/gi, /(\d+)\s*(minutes?|mins?)\s+ago/gi, /just\s+now/gi, /a\s+moment\s+ago/gi, ], // Breast side breastSide: [ /(left|right)\s+(breast|side|boob)/gi, /both\s+(breasts?|sides?|boobs?)/gi, ], // Diaper type diaperType: [ /\b(wet|pee|urine)\b/gi, /\b(dirty|poop|poopy|soiled|bowel\s+movement|bm)\b/gi, /\b(dry|clean)\b/gi, ], // Sleep type sleepType: [ /\b(nap|napping)\b/gi, /\b(night|bedtime|overnight)\b/gi, ], }; /** * Classifies the intent of a voice command */ export function classifyIntent(text: string): ClassificationResult { const normalizedText = text.toLowerCase().trim(); // Check each intent type let highestConfidence = 0; let matchedIntent = IntentType.UNKNOWN; for (const [intent, patterns] of Object.entries(INTENT_PATTERNS)) { let matchCount = 0; for (const pattern of patterns) { if (pattern.test(normalizedText)) { matchCount++; } } // Calculate confidence based on match count const confidence = Math.min(matchCount * 0.4, 1.0); if (confidence > highestConfidence) { highestConfidence = confidence; matchedIntent = intent as IntentType; } } // Extract entities based on intent const entities = extractEntities(normalizedText, matchedIntent); // Build structured data const structuredData = buildStructuredData(matchedIntent, entities, text); return { intent: matchedIntent, confidence: highestConfidence, entities, rawText: text, structuredData, }; } /** * Extracts entities from text based on intent */ function extractEntities(text: string, intent: IntentType): Entity[] { const entities: Entity[] = []; // Extract amount for (const pattern of ENTITY_PATTERNS.amount) { const matches = Array.from(text.matchAll(pattern)); for (const match of matches) { entities.push({ type: 'amount', value: parseFloat(match[1]), confidence: 0.9, text: match[0], }); } } // Extract duration for (const pattern of ENTITY_PATTERNS.duration) { const matches = Array.from(text.matchAll(pattern)); for (const match of matches) { entities.push({ type: 'duration', value: parseInt(match[1]), confidence: 0.9, text: match[0], }); } } // Extract time for (const pattern of ENTITY_PATTERNS.time) { const matches = Array.from(text.matchAll(pattern)); for (const match of matches) { entities.push({ type: 'time', value: match[0], confidence: 0.8, text: match[0], }); } } // Extract relative time for (const pattern of ENTITY_PATTERNS.relativeTime) { const matches = Array.from(text.matchAll(pattern)); for (const match of matches) { entities.push({ type: 'relativeTime', value: match[0], confidence: 0.85, text: match[0], }); } } // Intent-specific entities if (intent === IntentType.FEEDING) { // Extract breast side for (const pattern of ENTITY_PATTERNS.breastSide) { const matches = Array.from(text.matchAll(pattern)); for (const match of matches) { entities.push({ type: 'breastSide', value: match[1].toLowerCase(), confidence: 0.9, text: match[0], }); } } } if (intent === IntentType.DIAPER) { // Extract diaper type for (const pattern of ENTITY_PATTERNS.diaperType) { const matches = Array.from(text.matchAll(pattern)); for (const match of matches) { entities.push({ type: 'diaperType', value: match[1].toLowerCase(), confidence: 0.9, text: match[0], }); } } } if (intent === IntentType.SLEEP) { // Extract sleep type for (const pattern of ENTITY_PATTERNS.sleepType) { const matches = Array.from(text.matchAll(pattern)); for (const match of matches) { entities.push({ type: 'sleepType', value: match[1].toLowerCase(), confidence: 0.9, text: match[0], }); } } } return entities; } /** * Builds structured data from entities */ function buildStructuredData( intent: IntentType, entities: Entity[], rawText: string ): FeedingData | SleepData | DiaperData | undefined { if (intent === IntentType.FEEDING) { return buildFeedingData(entities, rawText); } else if (intent === IntentType.SLEEP) { return buildSleepData(entities, rawText); } else if (intent === IntentType.DIAPER) { return buildDiaperData(entities, rawText); } return undefined; } /** * Builds feeding data from entities */ function buildFeedingData(entities: Entity[], rawText: string): FeedingData { const data: FeedingData = { type: FeedingType.BOTTLE, // Default }; // Determine feeding type from text const lowerText = rawText.toLowerCase(); if (lowerText.includes('breast') || lowerText.includes('nurs')) { const breastSide = entities.find(e => e.type === 'breastSide'); if (breastSide) { if (breastSide.value === 'left') data.type = FeedingType.BREAST_LEFT; else if (breastSide.value === 'right') data.type = FeedingType.BREAST_RIGHT; else if (breastSide.value === 'both') data.type = FeedingType.BREAST_BOTH; data.side = breastSide.value; } else { data.type = FeedingType.BREAST_BOTH; // Default to both if not specified } } else if (lowerText.includes('solid') || lowerText.includes('food') || lowerText.includes('ate')) { data.type = FeedingType.SOLID; } else { data.type = FeedingType.BOTTLE; } // Extract amount const amountEntity = entities.find(e => e.type === 'amount'); if (amountEntity) { data.amount = amountEntity.value; // Determine unit from text if (rawText.toLowerCase().includes('ml') || rawText.toLowerCase().includes('milliliter')) { data.unit = 'ml'; } else if (rawText.toLowerCase().includes('oz') || rawText.toLowerCase().includes('ounce')) { data.unit = 'oz'; } else if (rawText.toLowerCase().includes('tbsp') || rawText.toLowerCase().includes('tablespoon')) { data.unit = 'tbsp'; } else { data.unit = 'ml'; // Default } } // Extract duration const durationEntity = entities.find(e => e.type === 'duration'); if (durationEntity) { data.duration = durationEntity.value; } // Extract time const timeEntity = entities.find(e => e.type === 'time' || e.type === 'relativeTime'); if (timeEntity) { data.startTime = parseTime(timeEntity.value); } return data; } /** * Builds sleep data from entities */ function buildSleepData(entities: Entity[], rawText: string): SleepData { const data: SleepData = { type: SleepType.NAP, // Default }; // Determine sleep type const sleepTypeEntity = entities.find(e => e.type === 'sleepType'); if (sleepTypeEntity) { if (sleepTypeEntity.value === 'night' || sleepTypeEntity.value === 'bedtime') { data.type = SleepType.NIGHT; } else { data.type = SleepType.NAP; } } // Extract duration const durationEntity = entities.find(e => e.type === 'duration'); if (durationEntity) { data.duration = durationEntity.value; } // Extract time const timeEntity = entities.find(e => e.type === 'time' || e.type === 'relativeTime'); if (timeEntity) { data.startTime = parseTime(timeEntity.value); } return data; } /** * Builds diaper data from entities */ function buildDiaperData(entities: Entity[], rawText: string): DiaperData { const data: DiaperData = { type: DiaperType.WET, // Default }; // Determine diaper type const diaperTypeEntities = entities.filter(e => e.type === 'diaperType'); const types = diaperTypeEntities.map(e => e.value); const hasWet = types.some(t => ['wet', 'pee', 'urine'].includes(t)); const hasDirty = types.some(t => ['dirty', 'poop', 'poopy', 'soiled', 'bowel', 'bm'].includes(t)); const hasDry = types.some(t => ['dry', 'clean'].includes(t)); if (hasWet && hasDirty) { data.type = DiaperType.BOTH; } else if (hasDirty) { data.type = DiaperType.DIRTY; } else if (hasDry) { data.type = DiaperType.DRY; } else { data.type = DiaperType.WET; } // Extract time const timeEntity = entities.find(e => e.type === 'time' || e.type === 'relativeTime'); if (timeEntity) { data.time = parseTime(timeEntity.value); } return data; } /** * Parses time expressions into Date objects */ function parseTime(timeText: string): Date { const now = new Date(); // Handle relative time if (timeText.includes('ago')) { const hoursMatch = timeText.match(/(\d+)\s*hours?/i); const minutesMatch = timeText.match(/(\d+)\s*minutes?/i); if (hoursMatch) { return new Date(now.getTime() - parseInt(hoursMatch[1]) * 60 * 60 * 1000); } if (minutesMatch) { return new Date(now.getTime() - parseInt(minutesMatch[1]) * 60 * 1000); } } // Handle "just now" or "a moment ago" if (timeText.includes('just now') || timeText.includes('moment ago')) { return now; } // Handle specific times (simplified - would need more robust parsing in production) const timeMatch = timeText.match(/(\d{1,2}):(\d{2})\s*(am|pm)?/i); if (timeMatch) { let hours = parseInt(timeMatch[1]); const minutes = parseInt(timeMatch[2]); const period = timeMatch[3]?.toLowerCase(); if (period === 'pm' && hours < 12) hours += 12; if (period === 'am' && hours === 12) hours = 0; const result = new Date(now); result.setHours(hours, minutes, 0, 0); return result; } // Default to now return now; } /** * Confidence scoring helper */ export function getConfidenceLevel(confidence: number): 'high' | 'medium' | 'low' { if (confidence >= 0.8) return 'high'; if (confidence >= 0.5) return 'medium'; return 'low'; } /** * Validates classification result */ export function validateClassification(result: ClassificationResult): boolean { return result.intent !== IntentType.UNKNOWN && result.confidence >= 0.3; }