File size: 18,897 Bytes
f75c5b2
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
a3fc1ff
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
"""
app.py โ€” Local Gradio app with Hindi speech-to-text support.
- English text input (Stage 2โ€“5 unchanged)
- English audio upload/record
- Hindi audio upload/record โ†’ Whisper translates to English โ†’ Stage 2โ€“5
"""

import os
import sys
from pathlib import Path

sys.path.insert(0, str(Path(__file__).parent))

from utils.config import config
from utils.logger import logger

# โ”€โ”€ Auto-download spaCy model if missing โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
def _ensure_spacy():
    try:
        import spacy
        spacy.load("en_core_web_sm")
    except OSError:
        logger.info("Downloading spaCy en_core_web_sm โ€ฆ")
        from spacy.cli import download
        download("en_core_web_sm")
        logger.info("spaCy model ready.")

_ensure_spacy()

# โ”€โ”€ Auto-train classifier if no saved model โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
def _ensure_model():
    from health_classifier.model import HealthClassifier
    from health_classifier.feature_engineering import generate_synthetic_training_data, FEATURE_NAMES
    clf = HealthClassifier(model_type="random_forest")
    if clf.load():
        logger.info("Loaded saved classifier.")
        return
    logger.info("No saved model โ€” training on synthetic data โ€ฆ")
    df = generate_synthetic_training_data(n_samples=1000)
    metrics = clf.train(df[FEATURE_NAMES], df["label"])
    clf.save()
    logger.info(f"Classifier ready. acc={metrics['test_accuracy']:.3f}")

_ensure_model()

# โ”€โ”€ Imports โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
import traceback
import gradio as gr
import pandas as pd

from recipe_nlp.extractor import RecipeExtractor
from nutrition_engine.mapper import NutritionMapper, NutritionAggregator
from health_classifier.model import HealthClassifier, LABEL_EMOJI, LABEL_NAMES
from health_classifier.explainer import RecipeExplainer
from health_classifier.feature_engineering import FeatureEngineer

# โ”€โ”€ Pipeline โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€

_BASE_PIPELINE = {
    "extractor":  RecipeExtractor(),
    "mapper":     NutritionMapper(),
    "aggregator": NutritionAggregator(),
    "classifier": HealthClassifier(),
    "fe":         FeatureEngineer(),
}


def run_pipeline(text: str):
    """Stages 2โ€“5 โ€” completely unchanged."""
    p = _BASE_PIPELINE

    try:
        structure = p["extractor"].extract(text)
    except Exception as e:
        raise Exception(f"NLP extraction failed: {e}")

    if not structure.ingredients:
        raise Exception(
            "No ingredients found. Try being more specific, "
            "e.g. '2 cups flour, 1 egg, 300g chicken'."
        )

    try:
        ing_nutritions = p["mapper"].map_ingredients(structure.ingredients)
        nutrition = p["aggregator"].aggregate(
            ing_nutritions, structure.servings_hint, structure.cooking_methods
        )
    except Exception as e:
        raise Exception(f"Nutrition mapping failed: {e}")

    try:
        features = p["fe"].extract(nutrition)
        label, score, probabilities = p["classifier"].predict(features)
    except Exception as e:
        raise Exception(f"Classification failed: {e}")

    try:
        explainer = RecipeExplainer(p["classifier"])
        explanation = explainer.explain(features, label, score, probabilities)
    except Exception as e:
        logger.warning(f"Explainer failed (non-fatal): {e}")
        explanation = None

    return label, score, probabilities, nutrition, structure, explanation


def transcribe_audio(audio_path: str, language: str = None, task: str = "transcribe") -> str:
    """
    Transcribe audio using Whisper.
    For Hindi โ†’ English: language="hi", task="translate"
    For English:         language=None,  task="transcribe"
    """
    try:
        from speech_module.transcriber1 import SpeechTranscriber
        transcriber = SpeechTranscriber()
        text, conf = transcriber.transcribe(audio_path, language=language, task=task)
        logger.info(f"Transcribed: lang={language or 'auto'} task={task} conf={conf:.2f}")
        return text
    except Exception as e:
        err = str(e)
        if "WinError 2" in err or "ffmpeg" in err.lower() or "No such file" in err:
            raise Exception(
                "ffmpeg not found. Download from https://ffmpeg.org, "
                "extract to C:\\ffmpeg, add C:\\ffmpeg\\bin to PATH, "
                "then restart the app."
            )
        raise Exception(f"Audio transcription failed: {e}")


# โ”€โ”€ UI helpers โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€

DAILY = config.classifier.daily_recommended
UNITS = {
    "calories": "kcal", "total_fat": "g", "saturated_fat": "g",
    "protein": "g", "carbohydrates": "g", "sugar": "g",
    "fiber": "g", "sodium": "mg",
}
NUTR_LABELS = {
    "calories": "๐Ÿ”ฅ Calories", "total_fat": "๐Ÿฅ‘ Total fat",
    "saturated_fat": "โš  Saturated fat", "protein": "๐Ÿ’ช Protein",
    "carbohydrates": "๐Ÿž Carbs", "sugar": "๐Ÿฌ Sugar",
    "fiber": "๐ŸŒพ Fiber", "sodium": "๐Ÿง‚ Sodium",
}


def _score_html(label: str, score: float, proba: dict) -> str:
    if score >= 7:
        clr, bg, text_clr, border_clr, emoji = "#22c55e", "#f0fdf4", "#14532d", "#bbf7d0", "๐ŸŸข"
    elif score >= 4:
        clr, bg, text_clr, border_clr, emoji = "#f59e0b", "#fffbeb", "#78350f", "#fde68a", "๐ŸŸก"
    else:
        clr, bg, text_clr, border_clr, emoji = "#ef4444", "#fef2f2", "#7f1d1d", "#fecaca", "๐Ÿ”ด"
    bar = max(0, min(100, score * 10))
    proba_rows = ""
    for lbl, p in sorted(proba.items(), key=lambda x: x[1], reverse=True):
        if not lbl:
            continue
        proba_rows += f"""
        <div style="display:flex;justify-content:space-between;align-items:center;
                    padding:6px 4px;border-bottom:1px solid {border_clr};
                    font-size:13px;color:#4b5563;">
            <span style="font-weight:600;color:#374151;">{lbl}</span>
            <span style="font-weight:700;color:{text_clr};background:rgba(255,255,255,0.7);
                         padding:2px 8px;border-radius:12px;">{p:.0%}</span>
        </div>"""
    return f"""
    <div style="font-family:system-ui,-apple-system,sans-serif;padding:32px 28px;
                border-radius:20px;background:{bg};border:1px solid {border_clr};
                text-align:center;max-width:420px;margin:0 auto;">
        <div style="font-size:48px;margin-bottom:4px;">{emoji}</div>
        <div style="font-size:12px;font-weight:700;color:#6b7280;
                    letter-spacing:0.1em;text-transform:uppercase;margin-bottom:12px;">
            Health Rating
        </div>
        <div style="font-size:72px;font-weight:800;color:{clr};line-height:1;
                    letter-spacing:-0.02em;margin-bottom:16px;">
            {score}<span style="font-size:24px;color:#9ca3af;font-weight:500;">/10</span>
        </div>
        <div style="background:{clr};color:white;padding:6px 16px;border-radius:999px;
                    font-size:13px;font-weight:700;text-transform:uppercase;
                    letter-spacing:0.05em;display:inline-block;margin-bottom:20px;">
            {label}
        </div>
        <div style="background:rgba(0,0,0,0.05);border-radius:999px;height:10px;
                    margin:0 0 20px;overflow:hidden;">
            <div style="background:{clr};width:{bar}%;height:100%;border-radius:999px;"></div>
        </div>
        <div style="background:rgba(255,255,255,0.6);border-radius:16px;
                    border:1px solid {border_clr};padding:16px;text-align:left;">
            <div style="font-size:11px;color:#6b7280;font-weight:700;
                        letter-spacing:0.08em;margin-bottom:12px;">CLASS PROBABILITIES</div>
            {proba_rows}
        </div>
    </div>"""


def _error_html(msg: str) -> str:
    return f"""
<div style="font-family:system-ui;padding:20px;border-radius:12px;
            background:#fef2f2;border:2px solid #ef4444;max-width:420px;margin:0 auto;">
  <div style="font-size:18px;font-weight:600;color:#991b1b;margin-bottom:8px;">โš  Error</div>
  <div style="font-size:13px;line-height:1.6;color:#7f1d1d;">{msg}</div>
</div>"""


def _empty_html() -> str:
    return """
<div style="font-family:system-ui;padding:32px;border-radius:16px;
            background:#f9fafb;border:2px dashed #e5e7eb;text-align:center;
            color:#9ca3af;max-width:420px;margin:0 auto;">
  <div style="font-size:40px;margin-bottom:10px;">๐Ÿฅ—</div>
  <div style="font-size:14px;">Results will appear here after analysis</div>
</div>"""


def _nutr_df(per_serving: dict) -> pd.DataFrame:
    rows = []
    for key, unit in UNITS.items():
        val = per_serving.get(key, 0)
        ref = DAILY.get(key, 1) or 1
        pct = val / ref * 100
        good = key in ("fiber", "protein")
        status = ("โœ… Good" if pct >= 20 else "โš ๏ธ Low" if pct >= 10 else "โŒ Low") if good else \
                 ("โŒ Very high" if pct > 75 else "โš ๏ธ High" if pct > 40 else "โœ… OK")
        rows.append({"Nutrient": NUTR_LABELS.get(key, key),
                     "Amount": f"{val:.1f} {unit}",
                     "% Daily value": f"{pct:.0f}%",
                     "Status": status})
    return pd.DataFrame(rows)


def _ing_df(structure) -> pd.DataFrame:
    if not structure or not structure.ingredients:
        return pd.DataFrame(columns=["Ingredient", "Quantity", "Method", "Flag"])
    rows = []
    for i in structure.ingredients:
        flag = "โš  High-risk" if i.is_high_risk else ("โœ“ Healthy" if i.is_healthy else "")
        rows.append({"Ingredient": i.name, "Quantity": i.quantity or "โ€”",
                     "Method": i.method or "โ€”", "Flag": flag})
    return pd.DataFrame(rows)


def _expl_html(explanation) -> str:
    if not explanation:
        return ""
    try:
        d = explanation.to_dict()
        factors_html = "".join(
            f'<div style="display:flex;gap:10px;align-items:flex-start;margin:6px 0;font-size:13px;color:#1f2937;">'
            f'<span style="color:{"#ef4444" if i["direction"]=="negative" else "#22c55e"};font-weight:700;flex-shrink:0;">'
            f'{"โœ—" if i["direction"]=="negative" else "โœ“"}</span><span>{i["message"]}</span></div>'
            for i in d.get("factors", [])[:5]
        )
        suggs_html = "".join(
            f'<div style="font-size:13px;color:#4b5563;margin:4px 0 4px 22px;">โ†’ {s}</div>'
            for s in d.get("suggestions", [])
        )
        sugg_section = (
            f"<div style='font-weight:600;font-size:14px;margin:14px 0 8px;color:#1f2937;'>"
            f"๐Ÿ’ก Suggestions</div>{suggs_html}" if suggs_html else ""
        )
        return f"""
<div style="font-family:system-ui;padding:16px;">
  <div style="font-weight:600;font-size:15px;margin-bottom:10px;color:#1f2937;">
    ๐Ÿ” Key health factors (SHAP)</div>
  {factors_html}{sugg_section}
</div>"""
    except Exception as e:
        logger.warning(f"Explanation render failed: {e}")
        return ""


EMPTY_DF = pd.DataFrame()
EXAMPLES = [
    "Take 2 cups of butter, deep fry 300g chicken thighs. Serve with 1 cup heavy cream sauce and 100g cheddar cheese.",
    "Grill 200g salmon. Serve over 1 cup brown rice with 200g steamed broccoli, half an avocado, 1 tbsp olive oil, and 100g spinach.",
    "Simmer 2 cups red lentils with 4 cups broth, 2 carrots, 2 celery stalks, 1 onion, 3 garlic cloves, and a handful of spinach.",
    "Cook 200g spaghetti. Fry 150g bacon. Mix 3 egg yolks with 100g parmesan and 1 cup heavy cream. Season with salt.",
]


# โ”€โ”€ Gradio handlers โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€

def analyze_text(recipe_text: str):
    if not recipe_text or not recipe_text.strip():
        return _error_html("Please enter a recipe."), EMPTY_DF, EMPTY_DF, ""
    try:
        label, score, proba, nutrition, structure, explanation = run_pipeline(recipe_text.strip())
        return (_score_html(label, score, proba), _nutr_df(nutrition.per_serving),
                _ing_df(structure), _expl_html(explanation))
    except Exception as e:
        logger.error(f"Text error: {e}\n{traceback.format_exc()}")
        return _error_html(str(e)), EMPTY_DF, EMPTY_DF, ""


def analyze_english_audio(audio_path):
    if not audio_path:
        return _error_html("Please upload an audio file."), EMPTY_DF, EMPTY_DF, "", ""
    try:
        text = transcribe_audio(audio_path, language=None, task="transcribe")
    except Exception as e:
        return _error_html(str(e)), EMPTY_DF, EMPTY_DF, "", ""
    if not text or not text.strip():
        return _error_html("Could not transcribe audio."), EMPTY_DF, EMPTY_DF, "", ""
    transcript_display = f"๐Ÿ“ข Transcribed (English):\n{text}"
    try:
        label, score, proba, nutrition, structure, explanation = run_pipeline(text.strip())
        return (_score_html(label, score, proba), _nutr_df(nutrition.per_serving),
                _ing_df(structure), _expl_html(explanation), transcript_display)
    except Exception as e:
        return _error_html(str(e)), EMPTY_DF, EMPTY_DF, "", transcript_display


def analyze_hindi_audio(audio_path):
    """
    Hindi audio handler.
    Whisper uses task='translate' + language='hi' to:
    1. Transcribe the Hindi speech
    2. Translate it to English
    All in one forward pass โ€” no separate translation model needed.
    The English output goes directly into Stage 2 spaCy NLP unchanged.
    """
    if not audio_path:
        return _error_html("Please upload a Hindi audio file."), EMPTY_DF, EMPTY_DF, "", ""
    try:
        text = transcribe_audio(audio_path, language="hi", task="translate")
    except Exception as e:
        return _error_html(str(e)), EMPTY_DF, EMPTY_DF, "", ""
    if not text or not text.strip():
        return _error_html("Could not transcribe Hindi audio. Please speak clearly."), EMPTY_DF, EMPTY_DF, "", ""
    transcript_display = f"๐Ÿ“ข Hindi โ†’ English:\n{text}"
    try:
        label, score, proba, nutrition, structure, explanation = run_pipeline(text.strip())
        return (_score_html(label, score, proba), _nutr_df(nutrition.per_serving),
                _ing_df(structure), _expl_html(explanation), transcript_display)
    except Exception as e:
        return _error_html(str(e)), EMPTY_DF, EMPTY_DF, "", transcript_display


# โ”€โ”€ Layout โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€

with gr.Blocks(title="๐Ÿฅ— Recipe Health Analyzer") as demo:

    gr.Markdown("""
    # ๐Ÿฅ— Recipe Health Analyzer
    **Pipeline:** Speech / Text โ†’ NLP โ†’ USDA Nutrition โ†’ ML Classification โ†’ SHAP Explainability
    
    Supports **English text**, **English audio**, and **Hindi audio** input.
    """)

    with gr.Tabs():

        with gr.Tab("๐Ÿ“ Text input"):
            with gr.Row():
                with gr.Column(scale=2):
                    text_in = gr.Textbox(
                        label="Recipe text",
                        placeholder="2 cups flour, 1 egg, 300g chicken breast, 1 tbsp olive oil, steamed broccoli",
                        lines=7,
                    )
                    text_btn = gr.Button("๐Ÿ”ฌ Analyze recipe", variant="primary", size="lg")
                    gr.Examples(examples=[[e] for e in EXAMPLES], inputs=text_in,
                                label="Example recipes (click to load)")
                with gr.Column(scale=2):
                    text_score = gr.HTML(value=_empty_html(), label="Health score")

        with gr.Tab("๐ŸŽ™๏ธ English audio"):
            with gr.Row():
                with gr.Column(scale=2):
                    eng_audio_in = gr.Audio(label="Upload or record English audio",
                                            type="filepath", sources=["upload", "microphone"])
                    eng_audio_btn = gr.Button("๐ŸŽ™๏ธ Transcribe & analyze", variant="primary", size="lg")
                    eng_audio_text = gr.Textbox(label="Transcription", lines=4,
                                                interactive=False,
                                                placeholder="Transcribed English text appears here.")
                with gr.Column(scale=2):
                    eng_audio_score = gr.HTML(value=_empty_html(), label="Health score")

        with gr.Tab("๐Ÿ‡ฎ๐Ÿ‡ณ Hindi audio"):
            gr.Markdown("""
            **เคนเคฟเค‚เคฆเฅ€ เคฎเฅ‡เค‚ เคฌเฅ‹เคฒเฅ‡เค‚** โ€” Speak your recipe in Hindi.  
            Whisper automatically transcribes and translates to English in one step.
            """)
            with gr.Row():
                with gr.Column(scale=2):
                    hin_audio_in = gr.Audio(label="Upload or record Hindi audio",
                                            type="filepath", sources=["upload", "microphone"])
                    hin_audio_btn = gr.Button("๐Ÿ‡ฎ๐Ÿ‡ณ Transcribe Hindi & analyze",
                                              variant="primary", size="lg")
                    hin_audio_text = gr.Textbox(label="Hindi โ†’ English translation", lines=4,
                                                interactive=False,
                                                placeholder="Whisper's English translation appears here.")
                with gr.Column(scale=2):
                    hin_audio_score = gr.HTML(value=_empty_html(), label="Health score")

    gr.Markdown("---")

    with gr.Row():
        nutr_table = gr.Dataframe(label="๐Ÿ“Š Nutrition per serving", interactive=False, wrap=True)
        ing_table  = gr.Dataframe(label="๐Ÿงช Identified ingredients", interactive=False, wrap=True)

    expl_out = gr.HTML(label="๐Ÿ” SHAP explanation")

    text_btn.click(fn=analyze_text, inputs=[text_in],
                   outputs=[text_score, nutr_table, ing_table, expl_out])

    eng_audio_btn.click(fn=analyze_english_audio, inputs=[eng_audio_in],
                        outputs=[eng_audio_score, nutr_table, ing_table, expl_out, eng_audio_text])

    hin_audio_btn.click(fn=analyze_hindi_audio, inputs=[hin_audio_in],
                        outputs=[hin_audio_score, nutr_table, ing_table, expl_out, hin_audio_text])

    gr.Markdown("""
    ---
    **Stack:** spaCy ยท USDA FoodData Central ยท scikit-learn RandomForest ยท SHAP ยท OpenAI Whisper ยท Gradio  
    *Hindi uses Whisper `task="translate"` โ€” no separate translation model required.*
    """)


if __name__ == "__main__":
    demo.launch(share=True,
        ssr_mode=False,)