@@ -52,19 +52,15 @@ def _load_feature_names(self):
5252 """Load feature names"""
5353 try :
5454 # Try v2 first
55- feature_path = self .local_model_dir / 'feature_names_v2_20251103 .pkl'
55+ feature_path = self .local_model_dir / 'feature_names_v2_20251120 .pkl'
5656 if not feature_path .exists ():
57- feature_path = self .local_model_dir / 'xgboost_feature_names_20251103 .pkl'
57+ feature_path = self .local_model_dir / 'xgboost_feature_names_20251120 .pkl'
5858
5959 with open (feature_path , 'rb' ) as f :
6060 self .feature_names = pickle .load (f )
6161
62- # Fix VitaminA field name (µ/μ -> ug for ASCII compatibility)
63- self .feature_names = [
64- 'VitaminA_ug_per_serving' if 'VitaminA_' in name and 'g_per_serving' in name
65- else name
66- for name in self .feature_names
67- ]
62+ # Normalize feature names to lowercase
63+ self .feature_names = [name .lower () for name in self .feature_names ]
6864
6965 logger .info (f"Loaded { len (self .feature_names )} feature names" )
7066 except Exception as e :
@@ -75,7 +71,7 @@ def _load_feature_names(self):
7571 def _load_offline_model (self ):
7672 """Load lightweight HistGradient model"""
7773 try :
78- model_path = self .local_model_dir / 'baseline_nutrition_model_v2_20251103 .pkl'
74+ model_path = self .local_model_dir / 'baseline_nutrition_model_v2_20251120 .pkl'
7975 if not model_path .exists ():
8076 logger .info ("Offline model not found in local uploads" )
8177 self .models ['offline' ] = {'available' : False }
@@ -105,7 +101,7 @@ def _load_offline_model(self):
105101 def _load_local_xgboost (self ):
106102 """Load local XGBoost model"""
107103 try :
108- model_path = self .local_model_dir / 'xgboost_nutrition_model_20251103 .pkl'
104+ model_path = self .local_model_dir / 'xgboost_nutrition_model_20251120 .pkl'
109105 if not model_path .exists ():
110106 logger .info ("Local XGBoost model not found in local uploads" )
111107 self .models ['local_xgboost' ] = {'available' : False }
@@ -329,10 +325,30 @@ def predict(
329325 elif self .models .get ('offline' , {}).get ('available' ):
330326 model_key = 'offline'
331327 else :
328+ # No trained models are available in this environment.
329+ # Provide a lightweight heuristic fallback so the API remains usable for testing.
330+ try :
331+ # Basic heuristic: estimate daily caloric needs as Energy_kcal_per_serving * meals per day
332+ meals = int (input_data .get ('mealsPerDay' ) or input_data .get ('meals_per_day' ) or 3 )
333+ energy = float (input_data .get ('Energy_kcal_per_serving' ) or input_data .get ('energy' ) or 250 )
334+ caloric_needs = float (max (800.0 , energy * meals ))
335+ except Exception :
336+ caloric_needs = 2000.0
337+
332338 return {
333- 'success' : False ,
334- 'error' : 'No models available' ,
335- 'status' : 'error'
339+ 'success' : True ,
340+ 'prediction' : {
341+ 'caloric_needs' : float (caloric_needs ),
342+ 'unit' : 'kcal/day' ,
343+ 'model' : 'heuristic-fallback' ,
344+ 'accuracy' : 'heuristic: approximate'
345+ },
346+ 'model_info' : {
347+ 'type' : 'heuristic' ,
348+ 'size' : 'n/a' ,
349+ 'mode' : 'fallback'
350+ },
351+ 'status' : 'fallback'
336352 }
337353 else :
338354 model_key = model_preference
@@ -350,15 +366,36 @@ def predict(
350366 try :
351367 # Prepare input
352368 df = pd .DataFrame ([input_data ])
369+
370+ # Normalize column names to match model expectations
371+ column_mapping = {
372+ 'energy_kcal_per_serving' : 'energy_kcal_per_serving' ,
373+ 'protein_g_per_serving' : 'protein_g_per_serving' ,
374+ 'fat_g_per_serving' : 'fat_g_per_serving' ,
375+ 'carbohydrates_g_per_serving' : 'carbohydrate_g_per_serving' ,
376+ 'fiber_g_per_serving' : 'fiber_g_per_serving' ,
377+ 'calcium_mg_per_serving' : 'calcium_mg_per_serving' ,
378+ 'iron_mg_per_serving' : 'iron_mg_per_serving' ,
379+ 'zinc_mg_per_serving' : 'zinc_mg_per_serving' ,
380+ 'vitamina_ug_per_serving' : 'vitamin_a_mcg_per_serving' ,
381+ 'vitaminc_mg_per_serving' : 'vitamin_c_mg_per_serving' ,
382+ 'potassium_mg_per_serving' : 'potassium_mg_per_serving' ,
383+ 'region_encoded' : 'region_encoded' ,
384+ 'condition_encoded' : 'condition_encoded' ,
385+ 'age_group_encoded' : 'age_group_encoded' ,
386+ 'season_encoded' : 'season_encoded'
387+ }
388+ df .columns = [col .lower () for col in df .columns ]
389+ df = df .rename (columns = column_mapping )
353390
354391 # If we have canonical feature order, apply it. Otherwise try model's
355392 # feature_names_in_ attribute. If neither present, pass df as-is.
356393 if self .feature_names :
357394 try :
358395 df = df [self .feature_names ]
359- except Exception :
396+ except Exception as e :
360397 # columns missing or mismatched; fall back
361- logger .warning ("Feature names present but input missing some columns; falling back to available input columns" )
398+ logger .warning (f "Feature names present but input missing some columns: { e } ; falling back to available input columns" )
362399 else :
363400 if hasattr (model , 'feature_names_in_' ):
364401 cols = list (getattr (model , 'feature_names_in_' ))
@@ -369,19 +406,23 @@ def predict(
369406
370407 # Make prediction
371408 prediction = model .predict (df )[0 ]
372-
373- # Determine status
409+
410+ # Determine status based on model key
374411 if model_key == 'huggingface' :
375412 status = 'online'
413+ elif model_key == 'local_xgboost' :
414+ status = 'local'
415+ elif model_key == 'offline' :
416+ status = 'offline'
376417 else :
377418 status = 'unknown'
378-
419+
379420 return {
380421 'success' : True ,
381422 'prediction' : {
382423 'caloric_needs' : float (prediction ),
383424 'unit' : 'kcal/day' ,
384- 'model' : f" { model_info ['type' ]} ( { status . upper () } )" ,
425+ 'model' : model_info ['type' ],
385426 'accuracy' : model_info ['accuracy' ]
386427 },
387428 'model_info' : {
0 commit comments