| import os |
| import sys |
| import torch |
| import pickle |
| import logging |
| import tempfile |
| import requests |
| import re |
| import asyncio |
| import aiohttp |
| from urllib.parse import quote_plus |
| from pytube import Search |
| from PIL import Image |
| from torchvision import transforms |
| from transformers import AutoTokenizer, AutoModelForSeq2SeqLM, pipeline, AutoModelForCausalLM |
| import gradio as gr |
| import pandas as pd |
| import plotly.express as px |
| from reportlab.lib.pagesizes import letter |
| from reportlab.lib import colors |
| from reportlab.platypus import SimpleDocTemplate, Paragraph, Spacer, Image as ReportLabImage |
| from reportlab.lib.styles import getSampleStyleSheet |
| from io import BytesIO |
| from langchain_huggingface import HuggingFacePipeline |
| from langchain_core.runnables.history import RunnableWithMessageHistory |
| from langchain_core.chat_history import InMemoryChatMessageHistory |
| from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder |
| from pydub import AudioSegment |
| from pydub.utils import which |
|
|
| |
| from args import get_parser |
| from model import get_model |
| from output_utils import prepare_output |
|
|
| |
| device = torch.device("cuda" if torch.cuda.is_available() else "cpu") |
| map_loc = None if torch.cuda.is_available() else "cpu" |
| logging.getLogger("pytube").setLevel(logging.ERROR) |
|
|
| |
| model_envit5_name = "VietAI/envit5-translation" |
| try: |
| tokenizer_envit5 = AutoTokenizer.from_pretrained(model_envit5_name) |
| model_envit5 = AutoModelForSeq2SeqLM.from_pretrained( |
| model_envit5_name, |
| torch_dtype=torch.float16 if torch.cuda.is_available() else torch.float32 |
| ).to(device) |
| pipe_envit5 = pipeline( |
| "text2text-generation", |
| model=model_envit5, |
| tokenizer=tokenizer_envit5, |
| device=0 if torch.cuda.is_available() else -1, |
| max_new_tokens=512, |
| do_sample=False |
| ) |
| except Exception as e: |
| print(f"Error loading Vietnamese model: {e}") |
| pipe_envit5 = None |
|
|
| models = { |
| "Japanese": {"model_name": "Helsinki-NLP/opus-mt-en-jap"}, |
| "Chinese": {"model_name": "Helsinki-NLP/opus-mt-en-zh"} |
| } |
|
|
| for lang in models: |
| try: |
| tokenizer = AutoTokenizer.from_pretrained(models[lang]["model_name"]) |
| model = AutoModelForSeq2SeqLM.from_pretrained( |
| models[lang]["model_name"], |
| torch_dtype=torch.float16 if torch.cuda.is_available() else torch.float32 |
| ).to(device) |
| models[lang]["pipe"] = pipeline( |
| "translation", |
| model=model, |
| tokenizer=tokenizer, |
| device=0 if torch.cuda.is_available() else -1, |
| max_length=512, |
| batch_size=4 if torch.cuda.is_available() else 1, |
| truncation=True |
| ) |
| except Exception as e: |
| print(f"Error loading {lang} model: {e}") |
| models[lang]["pipe"] = None |
|
|
| |
| chatbot_tokenizer = AutoTokenizer.from_pretrained("bigscience/bloomz-560m") |
| chatbot_model = AutoModelForCausalLM.from_pretrained( |
| "bigscience/bloomz-560m", |
| torch_dtype=torch.float16 if torch.cuda.is_available() else torch.float32 |
| ).to(device) |
|
|
| chatbot_pipeline = pipeline( |
| "text-generation", |
| model=chatbot_model, |
| tokenizer=chatbot_tokenizer, |
| device=0 if torch.cuda.is_available() else -1, |
| max_new_tokens=100, |
| do_sample=True, |
| temperature=0.6, |
| top_p=0.9, |
| pad_token_id=chatbot_tokenizer.eos_token_id, |
| batch_size=1 |
| ) |
| llm = HuggingFacePipeline(pipeline=chatbot_pipeline) |
|
|
| |
| prompt = ChatPromptTemplate.from_template(""" |
| You are a professional culinary assistant. You will answer the user's question directly based on the provided recipe. |
| Do not repeat the recipe or question in your answer. Be concise. |
| |
| Dish: {title} |
| Ingredients: {ingredients} |
| Instructions: {instructions} |
| |
| User Question: {question} |
| Answer: |
| """) |
|
|
|
|
| chain = prompt | llm |
| chat_histories = {} |
|
|
| def get_session_history(session_id): |
| if session_id not in chat_histories: |
| chat_histories[session_id] = InMemoryChatMessageHistory() |
| return chat_histories[session_id] |
|
|
| chatbot_chain = RunnableWithMessageHistory( |
| chain, |
| get_session_history, |
| input_messages_key="question", |
| history_messages_key="history" |
| ) |
|
|
| |
| current_recipe_context = {"context": "", "title": "", "ingredients": [], "instructions": [], "image": None} |
|
|
| |
| def format_recipe(title, ingredients, instructions, lang): |
| emoji = {"title": "π½οΈ", "ingredients": "π§", "instructions": "π"} |
| titles = { |
| "en": {"ingredients": "Ingredients", "instructions": "Instructions"}, |
| "ja": {"ingredients": "Ingredients (ζζ)", "instructions": "Instructions (δ½γζΉ)"}, |
| "zh": {"ingredients": "Ingredients (ι£ζ)", "instructions": "Instructions (ζ₯ιͺ€)"}, |
| "vi": {"ingredients": "Ingredients (NguyΓͺn liα»u)", "instructions": "Instructions (CΓ‘ch lΓ m)"}, |
| } |
|
|
| code_mapping = { |
| "English (original)": "en", |
| "Japanese": "ja", |
| "Chinese": "zh", |
| "Vietnamese": "vi", |
| } |
| code = code_mapping.get(lang, "en") |
|
|
| result = [f"### {emoji['title']} {title}", f"**{emoji['ingredients']} {titles[code]['ingredients']}:**"] |
| result.extend([f"- {i}" for i in ingredients]) |
| result.append(f"\n**{emoji['instructions']} {titles[code]['instructions']}:**") |
| result.extend([f"{i+1}. {step}" for i, step in enumerate(instructions)]) |
| return "\n".join(result) |
|
|
| def translate_section(text, lang): |
| if lang == "English (original)": |
| return text |
|
|
| if lang == "Vietnamese": |
| if pipe_envit5 is None: |
| return f"β Vietnamese translation model not available" |
| try: |
| max_chunk_length = 400 |
| if len(text) > max_chunk_length: |
| sentences = text.split('. ') |
| chunks = [] |
| current_chunk = "" |
| for sentence in sentences: |
| if len(current_chunk) + len(sentence) < max_chunk_length: |
| current_chunk += sentence + ". " |
| else: |
| chunks.append(current_chunk) |
| current_chunk = sentence + ". " |
| if current_chunk: |
| chunks.append(current_chunk) |
| else: |
| chunks = [text] |
|
|
| translated_chunks = [] |
| for chunk in chunks: |
| chunk = f"en-vi: {chunk}" |
| translated = pipe_envit5(chunk, max_new_tokens=512)[0]["generated_text"] |
| translated = translated.replace("vi: vi: ", "").replace("vi: Vi: ", "").replace("vi: ", "").strip() |
| translated_chunks.append(translated) |
|
|
| return " ".join(translated_chunks) |
| except Exception as e: |
| print(f"Vietnamese translation error: {e}") |
| return text |
|
|
| if models.get(lang, {}).get("pipe") is None: |
| return f"β Translation model for {lang} not available" |
|
|
| try: |
| max_chunk_length = 400 |
| if len(text) > max_chunk_length: |
| sentences = text.split('. ') |
| chunks = [] |
| current_chunk = "" |
| for sentence in sentences: |
| if len(current_chunk) + len(sentence) < max_chunk_length: |
| current_chunk += sentence + ". " |
| else: |
| chunks.append(current_chunk) |
| current_chunk = sentence + ". " |
| if current_chunk: |
| chunks.append(current_chunk) |
| else: |
| chunks = [text] |
|
|
| translated_chunks = [] |
| for chunk in chunks: |
| translated = models[lang]["pipe"](chunk, max_length=512)[0]["translation_text"] |
| translated_chunks.append(translated) |
|
|
| return " ".join(translated_chunks) |
| except Exception as e: |
| print(f"Translation error ({lang}): {e}") |
| return text |
|
|
| def translate_recipe(lang): |
| if not current_recipe_context["title"]: |
| return "β Please generate a recipe from an image first." |
| title = translate_section(current_recipe_context["title"], lang) |
| ingrs = [translate_section(i, lang) for i in current_recipe_context["ingredients"]] |
| instrs = [translate_section(s, lang) for s in current_recipe_context["instructions"]] |
| return format_recipe(title, ingrs, instrs, lang) |
|
|
| |
| def nutrition_analysis(ingredient_input): |
| ingredients = " ".join(ingredient_input.strip().split()) |
| api_url = f'https://api.api-ninjas.com/v1/nutrition?query={ingredients}' |
| headers = {'X-Api-Key': 'AHVy+tpkUoueBNdaFs9nCg==sFZTMRn8ikZVzx6E'} |
| response = requests.get(api_url, headers=headers) |
| if response.status_code != 200: |
| return "β API error or quota exceeded.", None, None, None |
| data = response.json() |
| df = pd.DataFrame(data) |
| numeric_cols = [] |
| for col in df.columns: |
| if col == "name": |
| continue |
| df[col] = pd.to_numeric(df[col], errors="coerce") |
| if df[col].notna().sum() > 0: |
| numeric_cols.append(col) |
| if df.empty or len(numeric_cols) < 3: |
| return "β οΈ Insufficient numerical data for charts (need at least 3 metrics).", None, None, None |
| draw_cols = numeric_cols[:3] |
| fig_bar = px.bar(df, x="name", y=draw_cols[0], title=f"Bar Chart: {draw_cols[0]}", text_auto=True) |
| pie_data = df[[draw_cols[1], "name"]].dropna() |
| if pie_data[draw_cols[1]].sum() > 0: |
| fig_pie = px.pie(pie_data, names="name", values=draw_cols[1], title=f"Pie Chart: {draw_cols[1]}") |
| else: |
| fig_pie = px.bar(title="β οΈ Insufficient data for pie chart") |
| fig_line = px.line(df, x="name", y=draw_cols[2], markers=True, title=f"Line Chart: {draw_cols[2]}") |
| return "β
Analysis successful!", fig_bar, fig_pie, fig_line |
|
|
| def load_recipe_ingredients(): |
| if not current_recipe_context["ingredients"]: |
| return "β οΈ No ingredients available. Generate a recipe first." |
| return "\n".join(current_recipe_context["ingredients"]) |
|
|
| |
| def clean_response(response): |
| |
| if "Answer:" in response: |
| response = response.split("Answer:")[-1] |
|
|
| |
| response = re.sub(r"Dish:.*?(Ingredients:|Instructions:).*?", "", response, flags=re.DOTALL) |
| response = re.sub(r"Ingredients:.*?(Instructions:).*?", "", response, flags=re.DOTALL) |
| response = re.sub(r"Instructions:.*", "", response, flags=re.DOTALL) |
|
|
| |
| response = re.sub(r"You are a professional culinary assistant.*?Answer:", "", response, flags=re.DOTALL) |
| |
| |
| response = re.sub(r"User Question:.*", "", response, flags=re.DOTALL) |
| |
| |
| return response.strip() |
|
|
|
|
| def validate_cooking_time(question, instructions): |
| |
| time_pattern = r"(\d+)\s*(minutes|minute)" |
| total_time = 0 |
| for instr in instructions: |
| matches = re.findall(time_pattern, instr) |
| for match in matches: |
| total_time += int(match[0]) |
| |
| |
| user_time = re.search(time_pattern, question) |
| if user_time: |
| user_minutes = int(user_time.group(1)) |
| if user_minutes != total_time: |
| return f"The recipe takes about {total_time} minutes to cook, not {user_minutes} minutes." |
| return None |
|
|
| def generate_chat_response(message, session_id="default"): |
| if not current_recipe_context["title"]: |
| return "Please generate a recipe from an image before asking about the dish." |
| |
| |
| correction = validate_cooking_time(message, current_recipe_context["instructions"]) |
| |
| response = chatbot_chain.invoke( |
| { |
| "title": current_recipe_context["title"], |
| "ingredients": ", ".join(current_recipe_context["ingredients"]), |
| "instructions": " ".join(current_recipe_context["instructions"]), |
| "question": message |
| }, |
| config={"configurable": {"session_id": session_id}} |
| ) |
| |
| response = clean_response(response) |
| if correction: |
| response = f"{correction} {response}" |
| |
| return response.strip() |
|
|
|
|
| def chat_with_bot(message, chat_history, session_id="default"): |
| if not message.strip(): |
| return "", chat_history |
| response = generate_chat_response(message, session_id) |
| chat_history.append({"role": "user", "content": message}) |
| chat_history.append({"role": "assistant", "content": response}) |
| return "", chat_history |
|
|
| |
| with open("ingr_vocab.pkl", 'rb') as f: |
| ingrs_vocab = pickle.load(f) |
| with open("instr_vocab.pkl", 'rb') as f: |
| vocab = pickle.load(f) |
|
|
| args = get_parser() |
| args.maxseqlen = 15 |
| args.ingrs_only = False |
| model_ic = get_model(args, len(ingrs_vocab), len(vocab)) |
| model_ic.load_state_dict(torch.load("modelbest.ckpt", map_location=map_loc, weights_only=True)) |
| model_ic.to(device).eval() |
|
|
| transform = transforms.Compose([ |
| transforms.Resize(256), |
| transforms.CenterCrop(224), |
| transforms.ToTensor(), |
| transforms.Normalize((0.485, 0.456, 0.406), (0.229, 0.224, 0.225)) |
| ]) |
|
|
| def generate_recipe(image): |
| if image is None: |
| return "β Please upload an image." |
| current_recipe_context["image"] = image |
| image = transform(image.convert("RGB")).unsqueeze(0).to(device) |
| with torch.no_grad(): |
| outputs = model_ic.sample(image, greedy=True, temperature=1.0, beam=-1, true_ingrs=None) |
| ids = (outputs['ingr_ids'].cpu().numpy(), outputs['recipe_ids'].cpu().numpy()) |
| outs, valid = prepare_output(ids[1][0], ids[0][0], ingrs_vocab, vocab) |
| if not valid['is_valid']: |
| return f"β Invalid recipe: {valid['reason']}" |
| current_recipe_context.update({ |
| "title": outs['title'], |
| "ingredients": outs['ingrs'], |
| "instructions": outs['recipe'] |
| }) |
| return format_recipe(outs['title'], outs['ingrs'], outs['recipe'], "English (original)") |
|
|
| |
| languages_tts = { |
| "English": "en", |
| "Chinese": "zh-CN", |
| "Japanese": "ja", |
| "Vietnamese": "vi", |
| } |
|
|
| async def fetch_tts_audio_async(session, chunk, lang_code): |
| url = f"https://translate.google.com/translate_tts?ie=UTF-8&q={quote_plus(chunk)}&tl={lang_code}&client=tw-ob" |
| headers = { |
| "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36", |
| "Referer": "https://translate.google.com/", |
| } |
| try: |
| async with session.get(url, headers=headers, timeout=10) as response: |
| response.raise_for_status() |
| return await response.read() |
| except Exception as e: |
| print(f"TTS Error for chunk: {e}") |
| return None |
|
|
| async def fetch_all_tts_audio(chunks, lang_code): |
| async with aiohttp.ClientSession() as session: |
| tasks = [fetch_tts_audio_async(session, chunk, lang_code) for chunk in chunks] |
| return await asyncio.gather(*tasks) |
|
|
| def google_tts(text, lang): |
| if not text or text.startswith("β"): |
| return None, gr.update(visible=False) |
| |
| |
| clean_text = text.replace("**", "").replace("###", "").replace("- ", "") |
| for emoji in ["π½οΈ", "π§", "π"]: |
| clean_text = clean_text.replace(emoji, "") |
| |
| |
| max_chunk_length = 200 |
| chunks = [clean_text[i:i+max_chunk_length] for i in range(0, len(clean_text), max_chunk_length)] |
| if not chunks: |
| return None, gr.update(visible=False) |
| |
| |
| lang_code = languages_tts.get(lang, "en") |
| audio_contents = asyncio.run(fetch_all_tts_audio(chunks, lang_code)) |
| |
| |
| audio_files = [] |
| for i, content in enumerate(audio_contents): |
| if content: |
| with tempfile.NamedTemporaryFile(suffix=".mp3", delete=False) as f: |
| f.write(content) |
| audio_files.append(f.name) |
| |
| if not audio_files: |
| return None, gr.update(visible=False) |
| |
| |
| if len(audio_files) == 1: |
| return audio_files[0], gr.update(visible=True) |
| |
| if which("ffmpeg"): |
| try: |
| combined = AudioSegment.empty() |
| for file in audio_files: |
| combined += AudioSegment.from_mp3(file) |
| output_file = tempfile.NamedTemporaryFile(suffix=".mp3", delete=False).name |
| combined.export(output_file, format="mp3") |
| for file in audio_files: |
| os.unlink(file) |
| return output_file, gr.update(visible=True) |
| except Exception as e: |
| print(f"Error combining audio files: {e}") |
| |
| for i in range(1, len(audio_files)): |
| os.unlink(audio_files[i]) |
| return audio_files[0], gr.update(visible=True) |
| else: |
| print("FFmpeg not found, returning first audio chunk.") |
| for i in range(1, len(audio_files)): |
| os.unlink(audio_files[i]) |
| return audio_files[0], gr.update(visible=True) |
|
|
| |
| def search_top_3_videos(keyword): |
| if not keyword.strip(): |
| return ["", "", ""] * 3 |
| try: |
| search = Search(f"How to make {keyword}") |
| results = search.results[:3] |
| embeds, titles, urls = [], [], [] |
| for video in results: |
| embed_html = f''' |
| <iframe width="520" height="320" |
| src="https://www.youtube.com/embed/{video.video_id}" |
| frameborder="0" allowfullscreen></iframe> |
| ''' |
| embeds.append(embed_html) |
| titles.append(video.title) |
| urls.append(f"https://www.youtube.com/watch?v={video.video_id}") |
| while len(embeds) < 3: |
| embeds.append("No video found") |
| titles.append("") |
| urls.append("") |
| return embeds + titles + urls |
| except Exception as e: |
| print(f"Video search error: {e}") |
| return ["", "", ""] * 3 |
|
|
| |
| def get_google_maps_search_url(dish_name, city="Ho Chi Minh City"): |
| query = f"{dish_name} in {city}" |
| url = f"https://www.google.com/maps/search/{query.replace(' ', '+')}" |
| return url |
|
|
| def search_and_show_link(dish): |
| if not dish.strip(): |
| return "Go to Google Maps", gr.update(visible=False) |
| url = get_google_maps_search_url(dish) |
| return url, gr.update(visible=True) |
|
|
| |
| def generate_pdf_recipe(): |
| if not current_recipe_context["title"]: |
| return None, "β Please generate a recipe from an image first." |
|
|
| output_file = "recipe.pdf" |
| doc = SimpleDocTemplate(output_file, pagesize=letter) |
| styles = getSampleStyleSheet() |
| story = [] |
|
|
| if current_recipe_context["image"]: |
| try: |
| img_buffer = BytesIO() |
| current_recipe_context["image"].save(img_buffer, format="PNG") |
| img_buffer.seek(0) |
| img = ReportLabImage(img_buffer, width=200, height=200) |
| story.append(img) |
| story.append(Spacer(1, 12)) |
| except Exception as e: |
| print(f"Error adding image to PDF: {e}") |
|
|
| story.append(Paragraph(current_recipe_context["title"], styles['Title'])) |
| story.append(Spacer(1, 12)) |
| story.append(Paragraph("Ingredients:", styles['Heading2'])) |
| for ingr in current_recipe_context["ingredients"]: |
| story.append(Paragraph(f"- {ingr}", styles['Normal'])) |
| story.append(Spacer(1, 12)) |
| story.append(Paragraph("Instructions:", styles['Heading2'])) |
| for i, instr in enumerate(current_recipe_context["instructions"], 1): |
| story.append(Paragraph(f"{i}. {instr}", styles['Normal'])) |
|
|
| doc.build(story) |
| return output_file, "β
Recipe saved as recipe.pdf" |
|
|
| |
| with gr.Blocks(theme=gr.themes.Soft(), title="AI Recipe Generator") as demo: |
| gr.Markdown(""" |
| # π³ AI Recipe Generator & Multilingual Cooking Assistant |
| Generate recipes from images, translate to multiple languages, get cooking videos, chat with a culinary assistant, analyze nutrition, and find restaurants! |
| """) |
|
|
| with gr.Tab("π· Generate Recipe"): |
| with gr.Row(): |
| with gr.Column(): |
| image_input = gr.Image(type="pil", label="Upload Dish Image", height=300) |
| gen_btn = gr.Button("Generate Recipe", variant="primary", elem_id="action-btn") |
| save_pdf_btn = gr.Button("Save as PDF", variant="secondary", elem_id="action-btn") |
| pdf_output = gr.File(label="Download Recipe PDF", interactive=False) |
| recipe_output = gr.Markdown("### Your recipe will appear here", elem_classes="recipe-box") |
| gen_btn.click(generate_recipe, inputs=image_input, outputs=recipe_output) |
| save_pdf_btn.click(fn=generate_pdf_recipe, outputs=[pdf_output, recipe_output]) |
|
|
| with gr.Tab("π Translate & TTS"): |
| with gr.Row(): |
| with gr.Column(): |
| lang_dropdown = gr.Dropdown( |
| choices=["English (original)", "Japanese", "Chinese", "Vietnamese"], |
| value="Japanese", |
| label="Select Language" |
| ) |
| with gr.Row(): |
| trans_btn = gr.Button("Translate Recipe", variant="primary", elem_id="action-btn") |
| tts_btn = gr.Button("π Listen to Recipe", variant="secondary", elem_id="action-btn") |
| with gr.Column(): |
| translation_output = gr.Markdown("### Translated recipe will appear here", elem_classes="recipe-box") |
| tts_audio = gr.Audio(interactive=False, label="Audio Output", visible=False) |
| trans_btn.click(fn=translate_recipe, inputs=lang_dropdown, outputs=translation_output) |
| tts_btn.click(fn=google_tts, inputs=[translation_output, lang_dropdown], outputs=[tts_audio, tts_audio]) |
|
|
| with gr.Tab("π₯ Cooking Videos"): |
| with gr.Row(): |
| with gr.Column(): |
| video_keyword = gr.Textbox(label="Search Cooking Videos", placeholder="e.g. beef pho") |
| search_btn = gr.Button("Search Videos", variant="primary", elem_id="action-btn") |
| with gr.Column(): |
| video_embeds, video_titles, video_urls = [], [], [] |
| for i in range(3): |
| with gr.Column(): |
| video_embeds.append(gr.HTML(label=f"π¬ Video {i+1}")) |
| video_titles.append(gr.Textbox(label=f"π Title {i+1}", interactive=False)) |
| video_urls.append(gr.Textbox(label=f"π URL {i+1}", interactive=False, visible=False)) |
| search_btn.click(fn=search_top_3_videos, inputs=video_keyword, outputs=video_embeds + video_titles + video_urls) |
|
|
| with gr.Tab("π¬ Culinary Chatbot"): |
| chatbot = gr.Chatbot(height=400, type="messages") |
| with gr.Row(): |
| chat_input = gr.Textbox(placeholder="Ask about the dish...", scale=4) |
| chat_btn = gr.Button("Send", variant="primary", scale=1, elem_id="action-btn") |
| chat_btn.click(chat_with_bot, inputs=[chat_input, chatbot], outputs=[chat_input, chatbot]) |
| chat_input.submit(chat_with_bot, inputs=[chat_input, chatbot], outputs=[chat_input, chatbot]) |
|
|
| with gr.Tab("π₯ Nutrition Analysis"): |
| with gr.Row(): |
| with gr.Column(): |
| ingredient_input = gr.Textbox( |
| label="π§Ύ Enter Ingredients (one per line or space-separated)", |
| lines=10, |
| placeholder="cheese\npepper\negg\n..." |
| ) |
| with gr.Row(): |
| load_ingredients_btn = gr.Button("Load Recipe Ingredients", variant="secondary", elem_id="action-btn") |
| analyze_btn = gr.Button("Analyze Nutrition", variant="primary", elem_id="action-btn") |
| with gr.Column(): |
| nutrition_message = gr.Textbox(label="π Message", interactive=False) |
| bar_chart = gr.Plot(label="π Bar Chart") |
| pie_chart = gr.Plot(label="π₯§ Pie Chart") |
| line_chart = gr.Plot(label="π Line Chart") |
| load_ingredients_btn.click(fn=load_recipe_ingredients, outputs=ingredient_input) |
| analyze_btn.click( |
| fn=nutrition_analysis, |
| inputs=ingredient_input, |
| outputs=[nutrition_message, bar_chart, pie_chart, line_chart] |
| ) |
|
|
| with gr.Tab("π½οΈ Find Restaurants"): |
| with gr.Row(): |
| with gr.Column(): |
| dish_input = gr.Textbox(label="Enter Dish Name", placeholder="e.g. beef pho", interactive=True) |
| search_restaurant_btn = gr.Button("Find Restaurants", variant="primary", elem_id="action-btn") |
| open_maps_btn = gr.Button("Go to Google Maps", visible=True, variant="secondary", elem_id="open-maps-btn") |
| search_restaurant_btn.click(fn=search_and_show_link, inputs=dish_input, outputs=[open_maps_btn, open_maps_btn]) |
| open_maps_btn.click( |
| fn=lambda url: url, |
| inputs=open_maps_btn, |
| outputs=None, |
| js="(url) => { if(url) window.open(url, '_blank'); }" |
| ) |
|
|
| demo.css = """ |
| .recipe-box { |
| padding: 20px; |
| border-radius: 10px; |
| background: #f9f9f9; |
| border: 1px solid #e0e0e0; |
| } |
| .dark .recipe-box { |
| background: #2a2a2a; |
| border-color: #444; |
| } |
| .gr-box { |
| margin-bottom: 20px; |
| } |
| #action-btn { |
| max-width: 220px; |
| margin: 10px auto; |
| font-weight: 600; |
| font-size: 16px; |
| border-radius: 8px; |
| } |
| #open-maps-btn { |
| max-width: 220px; |
| margin: 10px auto; |
| font-weight: 600; |
| font-size: 16px; |
| border-radius: 8px; |
| } |
| """ |
|
|
| if __name__ == "__main__": |
| demo.launch() |
|
|