Skip to content

Commit 8cf49a0

Browse files
full interface set up
1 parent 895b35e commit 8cf49a0

12 files changed

Lines changed: 437 additions & 325 deletions

File tree

backend/api/models/loader.py

Lines changed: 60 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -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': {

backend/api/routers/foods.py

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,8 @@ def get_local_foods(limit: int = 500):
4747
df = pd.read_csv(csv_path)
4848

4949
foods = []
50-
for _, row in df.iterrows():
50+
seen = set()
51+
for i, row in df.iterrows():
5152
try:
5253
name = row.get('food_name_english') or row.get('food_name') or ''
5354
category = _normalize_category(row.get('food_category', 'other'))
@@ -70,8 +71,17 @@ def get_local_foods(limit: int = 500):
7071
availability_score = float(row.get('availability_score', 0) or 0)
7172
available = availability_score >= 0.5
7273

74+
# normalize name for deduplication
75+
clean_name = str(name).strip()
76+
key = clean_name.lower()
77+
if key in seen:
78+
# skip duplicates by normalized name
79+
continue
80+
seen.add(key)
81+
7382
foods.append({
74-
'name': str(name),
83+
'id': f"food_{i}",
84+
'name': clean_name,
7585
'category': category,
7686
'region': region,
7787
'energy': round(energy, 1),

frontend/src/components/layout/AppHeader.vue

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -56,15 +56,17 @@ const navItems = computed(() => [
5656
</nav>
5757

5858
<div class="header-right">
59-
<!-- Online/Offline Status -->
59+
<!-- Online/Offline Status (click to toggle forced offline in settings) -->
6060
<div class="status-indicator">
61-
<span
61+
<button
6262
class="status-badge"
63-
:class="appStore.isOnline ? 'status-online' : 'status-offline'"
63+
:class="(settingsStore.offlineMode ? 'status-offline' : (appStore.isOnline ? 'status-online' : 'status-offline'))"
64+
@click="settingsStore.toggleOfflineMode()"
65+
title="Toggle forced offline mode"
6466
>
65-
<i :class="appStore.isOnline ? 'pi pi-wifi' : 'pi pi-ban'"></i>
66-
{{ appStore.isOnline ? t('common.online') : t('common.offline') }}
67-
</span>
67+
<i :class="(settingsStore.offlineMode ? 'pi pi-ban' : (appStore.isOnline ? 'pi pi-wifi' : 'pi pi-ban'))"></i>
68+
{{ settingsStore.offlineMode ? (t('common.offline') + ' (forced)') : (appStore.isOnline ? t('common.online') : t('common.offline')) }}
69+
</button>
6870
</div>
6971

7072
<!-- Language selection removed (single-language mode) -->

0 commit comments

Comments
 (0)