import os import re import json import asyncio import tempfile import gradio as gr from telethon import TelegramClient from dotenv import load_dotenv # ================== LOAD ENV ================== load_dotenv() # ================== CONFIG ================== BOT_USERNAME = "mealschatbot" api_id = int(os.getenv("TG_API_ID")) api_hash = os.getenv("TG_API_HASH") phone_number = os.getenv("TG_PHONE") # ================== PARSER ================== def parse_food_message(text: str): if "Calories" not in text: return None data = {} # Item name name_match = re.search(r"You added:\s*\*\*(.+?)\*\*", text) if name_match: data["item"] = name_match.group(1).strip() # Calories cal_match = re.search( r"\*\*Calories\*\*\s*\n+\s*(\d+)\s*kcal", text, re.IGNORECASE, ) if cal_match: data["calories_kcal"] = int(cal_match.group(1)) # Macros macros = {} for key, pattern in { "protein_g": r"Protein:\s*(\d+)g", "carbs_g": r"Carbs:\s*(\d+)g", "fat_g": r"Fat:\s*(\d+)g", }.items(): m = re.search(pattern, text) if m: macros[key] = int(m.group(1)) if macros: data["macros"] = macros # Ingredients ingredients = [] block = re.search( r"\*\*Likely Ingredients\*\*\s*\n+([\s\S]+?)(?:📊|$)", text, ) if block: for line in block.group(1).splitlines(): line = line.strip("• ").strip() if not line: continue m = re.match(r"(.+?)\s*\((\d+)g,\s*(\d+)kcal\)", line) if m: ingredients.append({ "name": m.group(1), "quantity_g": int(m.group(2)), "calories_kcal": int(m.group(3)), }) if ingredients: data["likely_ingredients"] = ingredients return data # ================== TELEGRAM HANDLER ================== async def analyze_image(image_path: str): async with TelegramClient("session", api_id, api_hash) as client: await client.start(phone=phone_number) bot = await client.get_entity(BOT_USERNAME) history = await client.get_messages(bot, limit=1) last_id_before = history[0].id if history else 0 await client.send_file(bot, image_path) await asyncio.sleep(8) replies = await client.get_messages( bot, min_id=last_id_before, limit=10, ) for msg in reversed(replies): if msg.text: parsed = parse_food_message(msg.text) if parsed: return parsed return {"error": "No food data detected"} # ================== GRADIO WRAPPER ================== def gradio_handler(image): with tempfile.NamedTemporaryFile(suffix=".png", delete=False) as f: image.save(f.name) result = asyncio.run(analyze_image(f.name)) return json.dumps(result, indent=2) # ================== UI ================== with gr.Blocks(title="Meal Image → Nutrition JSON") as demo: gr.Markdown("## 🍽️ Meal Image → Nutrition JSON") gr.Markdown("Upload a food image. Output will be **pure JSON only**.") image_input = gr.Image(type="pil", label="Upload Food Image") json_output = gr.Code(label="Parsed JSON", language="json") analyze_btn = gr.Button("Analyze") analyze_btn.click( fn=gradio_handler, inputs=image_input, outputs=json_output, ) demo.launch()