import logging log = logging.getLogger(__name__) from typing import Any import gradio as gr from PIL import Image # from src import config from src.agents.mise_en_place import identify_ingredients # from src.agents.progress_validator import validate # from src.agents.recipe_planner import plan_recipe, propose_dishes # from src.data.nutrition import compute_nutrition # from src.pipeline import Recipe from src.ui.components import ( DishOptions, IngredientChips, NutritionGrid, RecipeHero, StepCard, VerdictBadge, recipe_to_state, ) from src.ui.theme import CSS, theme def on_propose(fridge_image: Image.Image | None, state: dict | None) -> tuple[str, str, list[str], dict]: """Photo โ†’ ingredients โ†’ 3 dish options.""" state = state or {} ingredients = identify_ingredients(fridge_image) # options = propose_dishes(ingredients) # state.update({ # "ingredients_have": ingredients, # "ingredients_missing": [], # "options": [o.model_dump() for o in options], # }) chips_html = IngredientChips.render({"have": ingredients, "missing": []}) log.info(ingredients) # options_html = DishOptions.render({"options": state["options"]}) # radio_choices = [o.name for o in options] # return chips_html, options_html, gr.update(choices=radio_choices, value=radio_choices[0] if radio_choices else None), state return chips_html # ---------------- # UI definition # ---------------- def build_ui() -> gr.Blocks: initial_state: dict[str, Any] = {} with gr.Blocks(title="Cook With Me") as demo: gr.Markdown( "# ๐Ÿฒ Cook With Me\n" "_A multimodal sous-chef. See it. Plan it. Show it. Cook it._" ) state = gr.State(initial_state) with gr.Tabs(): # --- Tab 1: Cook ------------------------------------------------ with gr.Tab("Cook"): with gr.Row(): with gr.Column(scale=1): fridge_input = gr.Image( label="๐Ÿ“ธ Photo of your fridge or pantry", type="pil", height=320, ) propose_btn = gr.Button("What can I cook?", variant="primary") gr.Markdown("### Ingredients I see") chips = gr.HTML(IngredientChips.render({})) gr.Markdown("### Pick a dish") options = gr.HTML(DishOptions.render({})) dish_radio = gr.Radio(choices=[], label="Choose one", interactive=True) with gr.Accordion("Generation options", open=False): illustrate_chk = gr.Checkbox(value=False, label="Render step images (FLUX, slow on CPU)") narrate_chk = gr.Checkbox(value=False, label="Generate voice narration (VoxCPM2)") cook_btn = gr.Button("Build recipe", variant="primary") with gr.Column(scale=2): hero = gr.HTML(RecipeHero.render({})) steps_panel = gr.HTML(StepCard.render({})) nutrition_panel = gr.HTML(NutritionGrid.render({"nutrition": {}})) # --- Tab 2: Check Progress ------------------------------------- with gr.Tab("Check Progress"): gr.Markdown("Upload a photo of your pan or plate; the same vision model that planned your recipe will compare it against the target step.") with gr.Row(): with gr.Column(): step_idx = gr.Number(value=1, precision=0, label="Active step #") progress_input = gr.Image(label="๐Ÿ“ธ Your pan / plate", type="pil", height=320) validate_btn = gr.Button("How am I doing?", variant="primary") with gr.Column(): verdict_panel = gr.HTML(VerdictBadge.render({})) verdict_audio = gr.Audio(label="Tip (voice)", autoplay=False) # --- Tab 3: About ---------------------------------------------- with gr.Tab("About"): gr.Markdown( """ ### Models - **Vision** โ€” `openbmb/MiniCPM-V-4_6-gguf` via `llama-cpp-python` (~4.6B) - **Planner** โ€” `openbmb/MiniCPM-V-4-gguf` via `llama-cpp-python` (~4B) - **Illustrator** โ€” `black-forest-labs/FLUX.2-klein-9B` via `diffusers` (9B) - **Narrator** โ€” `openbmb/VoxCPM2` via `transformers` (~1B) - **Retrieval** โ€” `sentence-transformers/all-MiniLM-L6-v2` (22M) **Total โ‰ˆ 18.6B params** (โ‰ค 32B requirement โœ“). ### Pipeline ``` Fridge photo โ†’ Vision โ†’ ingredients โ”‚ โ–ผ Planner (+ Kaggle retrieval) โ†’ Recipe JSON โ”‚ โ–ผ Illustrator (FLUX) โ†’ hero + per-step images โ”‚ โ–ผ Narrator (VoxCPM2) โ†’ MP3 per step โ”‚ โ–ผ Progress photo โ†’ Validator (same vision model) โ†’ go|wait|fix ``` ### Badges targeted โœ“ Llama Champion ยท โœ“ Well-Tuned ยท โœ“ Off-Brand ยท โœ“ Sharing is Caring ยท โœ“ Field Notes """ ) # Wire callbacks ---------------------------------------------------- propose_btn.click( fn=on_propose, inputs=[fridge_input, state], # outputs=[chips, options, dish_radio, state], outputs=[chips], ) # cook_btn.click( # fn=on_pick_dish, # inputs=[state, dish_radio, illustrate_chk, narrate_chk], # outputs=[hero, steps_panel, nutrition_panel, chips, state], # ) # validate_btn.click( # fn=on_validate, # inputs=[state, step_idx, progress_input], # outputs=[verdict_panel, verdict_audio], # ) return demo if __name__ == "__main__": demo = build_ui() demo.launch( server_name="0.0.0.0", server_port=int(__import__("os").environ.get("PORT", 7860)), show_error=True, inbrowser=True, theme=theme, css=CSS )