FoodLens: Multimodal Meal Coach

Deploy a public AI meal coach that analyzes photos with explicit uncertainty.

Introduction

30 Second Summary

A meal photo can make a dish look easy to understand at a glance. Hidden ingredients, cooking methods, and portion weight remain out of frame.

In this project, you will publish FoodLens as a public meal-photo coach on Hugging Face Spaces. A Qwen3-VL model on Hugging Face ZeroGPU turns a meal image plus written context into structured guidance with explicit uncertainty.

What You'll Build

Your public FoodLens page turns a visitor's meal photo plus context into a structured report that separates visible evidence from estimates plus unknowns.

By the end of this project, you'll have:

  • Share a public FoodLens URL that visitors can open without access to your computer.
  • Upload or capture a meal photo to receive a likely dish description plus a list of visible ingredients.
  • Add dietary preferences plus portion context to receive recipe adaptations with rough nutrition estimates. See explicit limits covering hidden ingredients, allergens, preparation methods, and medical suitability.
  • Secret Mission: add a second-pass evidence audit that challenges the model's first answer.

Are there any prerequisites?

You need internet access plus an eligible Hugging Face account with a verified email that is more than 30 days old. Bring a meal photo that contains no private information.

Before We Start

Before the hands-on work begins, lock in what FoodLens analyzes. You will also define why its nutrition guidance must remain a rough estimate.

Create a Public ZeroGPU Space

FoodLens needs a public home before model code enters the project. A small starter page proves that visitors can reach the deployment path.

The Hugging Face Spaces platform stores the app in a hosted repository. ZeroGPU supplies shared GPU capacity for later inference.

For this first deployment, Gradio turns a short Python file into a visible web page. The starter page isolates hosting from the model work that comes next.

In this step, get ready to:
  • Create a public Hugging Face Space named FoodLens.
  • Configure shared ZeroGPU hardware.
  • Publish a pinned Gradio starter page.
Create the public Space

A Space combines repository files with a hosted application. Public visibility gives visitors access without sharing your computer.

  • Sign in to your Hugging Face account in the web interface.
  • Select Spaces in the top navigation.
  • Select Create new Space.
  • Enter FoodLens as the Space name.
  • Select Public visibility.
  • Select Gradio for the SDK.
  • Submit the creation form to create the Space.

Good, your public FoodLens repository now exists. The hardware choice prepares its inference function for shared GPU access.

  • Select the Settings tab in the Space repository.
  • Select ZeroGPU as the Space hardware.

The repository header identifies FoodLens as public. The hardware setting shows ZeroGPU.

Why prove hosting first?

The starter page tests the repository-to-app deployment path with very little code. Later model issues stay separate from hosting issues.

Pin the Space configuration

The YAML front matter in README.md tells the Space how to build the app. Pinned values make future rebuilds use the same environment.

  • Select the Files tab in the Space repository.
  • Select Add file.
  • Choose the option to create a file.
  • Name the new file README.md.
  • Set the deployment metadata by pasting this content:
---
title: FoodLens
sdk: gradio
sdk_version: 6.30.0
python_version: 3.12.12
app_file: app.py
---

# FoodLens

A public multimodal meal coach. Results are estimates, not medical advice.

What does the Space metadata control?

  • The sdk value selects Gradio as the Space SDK.
  • The sdk_version value pins Gradio to 6.30.0.
  • The python_version value pins Python to 3.12.12.
  • The app_file value identifies app.py as the application entry point.
  • The final sentence sets the safety boundary visitors see in the repository.
  • Use the repository commit form to save README.md.
  • Confirm that README.md appears in the Files tab.

README metadata not recognised?

Check that the filename is exactly README.md. Check that both --- delimiter lines remain in the file.

Help me check my FoodLens Space metadata.

✔️ Awesome, I've got everything!

Your README.md file now pins the Space deployment environment.

ⓧ I'd like to double check the full code

---
title: FoodLens
sdk: gradio
sdk_version: 6.30.0
python_version: 3.12.12
app_file: app.py
---

# FoodLens

A public multimodal meal coach. Results are estimates, not medical advice.

The Space also needs a dependency list. Each pin locks one package to the version used by FoodLens.

  • Select Add file in the Files tab.
  • Choose the option to create a file.
  • Name the new file requirements.txt.
  • Pin the three Python dependencies by pasting this content:
torch==2.13.0
transformers==5.19.0
qwen-vl-utils==0.0.14

What do these dependencies provide?

  • The torch package provides the PyTorch runtime used by the model.
  • The transformers package provides the Hugging Face Transformers model classes used later.
  • The qwen-vl-utils package provides qwen-vl-utils preprocessing for visual inputs.
  • Each line locks its dependency to the version shown.
  • Use the repository commit form to save requirements.txt.
  • Confirm that requirements.txt appears beside README.md in the Files tab.

Dependency file missing?

Check that the filename is exactly requirements.txt. Confirm that the repository commit completed before leaving the file editor.

Help me find my missing FoodLens dependency file.

✔️ Awesome, I've got everything!

Your requirements.txt file now pins all three Python dependencies.

ⓧ I'd like to double check the full code

torch==2.13.0
transformers==5.19.0
qwen-vl-utils==0.0.14
Publish the starter page

The app.py file is the entry point named in the Space metadata. This starter creates one visible page with a deployment status message.

  • Select Add file in the Files tab.
  • Choose the option to create a file.
  • Name the new file app.py.
  • Create the starter Gradio page by pasting this code:
import gradio as gr


with gr.Blocks(title="FoodLens") as demo:
    gr.Markdown("# FoodLens\nDeployment is working.")


if __name__ == "__main__":
    demo.launch()

What does this code do?

  • The import gradio as gr line makes Gradio components available.
  • The gr.Blocks context creates the FoodLens page.
  • The gr.Markdown component renders the heading plus the deployment message.
  • The demo.launch() call starts the app when the Space runs app.py.
  • Use the repository commit form to save app.py.

The first rebuild can take several minutes while the Space installs the pinned dependencies. A visible build status means the deployment is still progressing.

Before you select the App tab, what two lines do you expect the starter page to show? Hold that prediction.

  • Select the App tab.
  • Keep the browser tab open until the automatic rebuild completes.

When the build completes, you see FoodLens above Deployment is working..

That is the public hosting path proven. Your repository now builds into a visible Gradio page.

Starter page not loading?

Confirm that the entry file is named exactly app.py. Compare its contents with the full file below.

If the build is still running, wait for its status to finish before refreshing the App tab.

Help me diagnose my FoodLens starter deployment.

✔️ Awesome, I've got everything!

Your saved app.py file matches the deployed starter page.

ⓧ I'd like to double check the full code

import gradio as gr


with gr.Blocks(title="FoodLens") as demo:
    gr.Markdown("# FoodLens\nDeployment is working.")


if __name__ == "__main__":
    demo.launch()

Your public Space is live with a reproducible starter environment. Next, you will connect a meal photo to the vision-language model for its first public description.

Get Your First Public Food Description

Your public FoodLens starter is live on Hugging Face Spaces. Visitors can already reach the starter page from a browser.

Now FoodLens needs its first real multimodal result. In this step, Qwen3-VL connects a meal photo to language before you add the safety structure later.

In this step, get ready to:
  • Link and preload the Qwen3-VL model through the Space configuration.
  • Build a ZeroGPU inference function for a meal photo plus a text prompt.
  • Generate your first public food description through Gradio.
Link and preload the model

The models field links the Space to its model repository. The preload_from_hub field caches the same model during the build.

  • Switch back to the Files tab from earlier.
  • Select README.md from the repository file list.
  • Enter edit mode using the file editor control.
  • Replace the contents of README.md with this configuration:
---
title: FoodLens
sdk: gradio
sdk_version: 6.30.0
python_version: 3.12.12
app_file: app.py
models:
  - Qwen/Qwen3-VL-2B-Instruct
preload_from_hub:
  - Qwen/Qwen3-VL-2B-Instruct
---

# FoodLens

A public multimodal meal coach. Results are estimates, not medical advice.

What does this configuration do?

  • The existing fields keep the Gradio SDK connected to app.py.
  • The models entry links Qwen/Qwen3-VL-2B-Instruct to the Space.
  • The preload_from_hub entry prepares the model files during the build.

✔️ Awesome, I've got everything!

Your Space configuration now links and preloads the model. Double-check that you save the updated file.

ⓧ I'd like to double check the full code

---
title: FoodLens
sdk: gradio
sdk_version: 6.30.0
python_version: 3.12.12
app_file: app.py
models:
  - Qwen/Qwen3-VL-2B-Instruct
preload_from_hub:
  - Qwen/Qwen3-VL-2B-Instruct
---

# FoodLens

A public multimodal meal coach. Results are estimates, not medical advice.
  • Submit the README.md update using the repository commit control.
  • Confirm the saved file shows both model metadata fields.
  • Return to the App tab to confirm that another automatic rebuild starts.

The new build now knows which model FoodLens needs. That prepares the Space for its first vision-language request.

Model metadata missing after the commit?

Check that models and preload_from_hub sit inside the YAML section between the two --- lines.

Confirm that each field contains the exact model ID Qwen/Qwen3-VL-2B-Instruct.

Help me check my Space metadata.

Build the ZeroGPU inference function

FoodLens loads the processor and model when the app starts. The GPU decorator reserves shared acceleration around the inference function.

Why one multimodal model?

Qwen3-VL accepts an image beside text in the same conversation. One model keeps this first inference path focused on image-to-language reasoning.

  • Switch back to the Files tab.
  • Select app.py from the repository file list.
  • Enter edit mode using the file editor control.
  • Begin one continuous edit by replacing everything above the current with gr.Blocks(title="FoodLens") as demo: line with this model setup:
import gradio as gr
import spaces
from transformers import AutoProcessor, Qwen3VLForConditionalGeneration
from qwen_vl_utils import process_vision_info

MODEL_ID = "Qwen/Qwen3-VL-2B-Instruct"

processor = AutoProcessor.from_pretrained(MODEL_ID)
model = Qwen3VLForConditionalGeneration.from_pretrained(
    MODEL_ID,
    dtype="auto",
)
model.to("cuda")

What does this model setup do?

  • The imports provide the interface components and shared GPU scheduler.
  • The imports also provide the model processor and multimodal preprocessing helper.
  • The MODEL_ID value keeps both model-loading calls pointed at the same repository.
  • The model moves to cuda so inference uses the allocated GPU.
  • Continue the same edit two blank lines below model.to("cuda") with the first part of run_model():
@spaces.GPU(duration=60)
def run_model(image, prompt, max_new_tokens=512):
    if image is None:
        return "Please upload or capture a meal photo."

    messages = [
        {
            "role": "user",
            "content": [
                {"type": "image", "image": image},
                {"type": "text", "text": prompt},
            ],
        }
    ]

    text = processor.apply_chat_template(
        messages,
        tokenize=False,
        add_generation_prompt=True,
    )
    image_inputs, video_inputs, video_kwargs = process_vision_info(
        messages,
        image_patch_size=16,
        return_video_kwargs=True,
        return_video_metadata=True,
    )

How does the request take shape?

  • The decorator allocates GPU time when run_model() receives a request.
  • The first check returns a clear message when the image input is empty.
  • The message places the uploaded image beside the visitor's prompt in one user turn.
  • The chat template formats that turn for the instruction model.
  • The vision helper prepares the image data required by the processor.
  • Continue run_model() one blank line below the vision preprocessing call with this tensor preparation:
    if video_inputs is not None:
        video_inputs, video_metadatas = zip(*video_inputs)
        video_inputs = list(video_inputs)
        video_metadatas = list(video_metadatas)
    else:
        video_metadatas = None

    inputs = processor(
        text=text,
        images=image_inputs,
        videos=video_inputs,
        video_metadata=video_metadatas,
        return_tensors="pt",
        do_resize=False,
        **video_kwargs,
    )
    inputs = inputs.to(model.device)

How are the inputs prepared?

  • The video branch preserves metadata when a request includes video content.
  • The processor converts the formatted text and visual inputs into model-ready tensors.
  • The final line moves those tensors onto the same device as the model.
  • Finish run_model() one blank line below inputs = inputs.to(model.device) with this generation logic:
    generated_ids = model.generate(**inputs, max_new_tokens=max_new_tokens)
    generated_ids_trimmed = [
        output_ids[len(input_ids) :]
        for input_ids, output_ids in zip(inputs.input_ids, generated_ids)
    ]
    return processor.batch_decode(
        generated_ids_trimmed,
        skip_special_tokens=True,
        clean_up_tokenization_spaces=False,
    )[0]

How does FoodLens return text?

  • The model generates response tokens up to the configured limit.
  • The trimming step removes the original input tokens from each generated sequence.
  • The decoder converts the remaining tokens into the food description returned to Gradio.
  • Keep two blank lines between the end of run_model() and the existing with gr.Blocks(title="FoodLens") as demo: line.

This build performs the first full model load. A longer wait than the starter build is normal while the Space prepares the model.

  • Submit the app.py update using the repository commit control.
  • Return to the App tab.
  • Keep the App tab open until the starter page returns.

You should see FoodLens with Deployment is working. again. That confirms the new imports and model setup can load inside the deployed Space.

Starter page not returning?

Compare every import with the code above. A misspelled class name can stop the app before the page loads.

Check that MODEL_ID matches Qwen/Qwen3-VL-2B-Instruct exactly.

Help me debug the model-loading code.

Connect the public interface

Gradio event wiring sends component values into run_model(). The image component supplies a PIL meal image while the text box supplies the prompt.

  • Return to the Files tab.
  • Select app.py from the repository file list.
  • Enter edit mode using the file editor control.
  • Replace the current with gr.Blocks(title="FoodLens") as demo: section through the end of the file with this interface:
with gr.Blocks(title="FoodLens") as demo:
    gr.Markdown("# FoodLens")
    image_input = gr.Image(type="pil", label="Meal photo")
    prompt_input = gr.Textbox(label="Prompt")
    analyze_button = gr.Button("Describe food")
    output = gr.Markdown()
    analyze_button.click(
        fn=run_model,
        inputs=[image_input, prompt_input],
        outputs=output,
    )


if __name__ == "__main__":
    demo.launch()

What does this interface do?

  • The image component converts an uploaded meal photo into a PIL image.
  • The prompt component collects the visitor's text request.
  • The button click passes both inputs to run_model().
  • The Markdown output renders the returned food description.

✔️ Awesome, I've got everything!

Your model pipeline and public interface are connected. Double-check that you save the completed app.py file.

ⓧ I'd like to double check the full code

import gradio as gr
import spaces
from transformers import AutoProcessor, Qwen3VLForConditionalGeneration
from qwen_vl_utils import process_vision_info

MODEL_ID = "Qwen/Qwen3-VL-2B-Instruct"

processor = AutoProcessor.from_pretrained(MODEL_ID)
model = Qwen3VLForConditionalGeneration.from_pretrained(
    MODEL_ID,
    dtype="auto",
)
model.to("cuda")


@spaces.GPU(duration=60)
def run_model(image, prompt, max_new_tokens=512):
    if image is None:
        return "Please upload or capture a meal photo."

    messages = [
        {
            "role": "user",
            "content": [
                {"type": "image", "image": image},
                {"type": "text", "text": prompt},
            ],
        }
    ]

    text = processor.apply_chat_template(
        messages,
        tokenize=False,
        add_generation_prompt=True,
    )
    image_inputs, video_inputs, video_kwargs = process_vision_info(
        messages,
        image_patch_size=16,
        return_video_kwargs=True,
        return_video_metadata=True,
    )

    if video_inputs is not None:
        video_inputs, video_metadatas = zip(*video_inputs)
        video_inputs = list(video_inputs)
        video_metadatas = list(video_metadatas)
    else:
        video_metadatas = None

    inputs = processor(
        text=text,
        images=image_inputs,
        videos=video_inputs,
        video_metadata=video_metadatas,
        return_tensors="pt",
        do_resize=False,
        **video_kwargs,
    )
    inputs = inputs.to(model.device)

    generated_ids = model.generate(**inputs, max_new_tokens=max_new_tokens)
    generated_ids_trimmed = [
        output_ids[len(input_ids) :]
        for input_ids, output_ids in zip(inputs.input_ids, generated_ids)
    ]
    return processor.batch_decode(
        generated_ids_trimmed,
        skip_special_tokens=True,
        clean_up_tokenization_spaces=False,
    )[0]


with gr.Blocks(title="FoodLens") as demo:
    gr.Markdown("# FoodLens")
    image_input = gr.Image(type="pil", label="Meal photo")
    prompt_input = gr.Textbox(label="Prompt")
    analyze_button = gr.Button("Describe food")
    output = gr.Markdown()
    analyze_button.click(
        fn=run_model,
        inputs=[image_input, prompt_input],
        outputs=output,
    )


if __name__ == "__main__":
    demo.launch()

The completed rebuild loads the full model before showing the interface. Expect this build to take several minutes while the model files are prepared.

  • Submit the completed app.py update using the repository commit control.
  • Return to the App tab.
  • Keep the page open until the rebuilt FoodLens interface loads.

You should see a Meal photo input above a Prompt field. You should also see the Describe food button.

Interface missing after the rebuild?

Check that the interface block remains outside run_model(). The line beginning with with gr.Blocks must start at the left edge.

Confirm that fn=run_model matches the function name exactly.

Help me debug the FoodLens interface.

The interface now has everything needed for its first image-to-text result. Shared GPU scheduling can make the first request pause while capacity is assigned.

  • Upload a meal photo that contains no private information through the Meal photo input.
  • Enter Describe the food in this image. List the likely dish and visible ingredients. in the Prompt field.

Before you submit, which food details do you expect the model can name directly from the pixels?

  • Click Describe food.
  • Keep the page open if the request enters a queue.

You should see an output that names a likely dish or identifies visible foods. You now have a public model connecting a real meal photo to language.

FoodLens can now describe a meal from an uploaded image. Next, you will test how confidently the model speaks when the photo cannot prove the answer.

Expose the Confidence Problem

Your public FoodLens app can already turn a meal photo into a likely food description. The deployed Qwen3-VL model is successfully connecting image evidence to language.

Nutrition makes a stronger demand on those same pixels. This step asks for exact values so you can test whether the answer stays grounded in what the photo proves.

In this step, get ready to:
  • Submit an exact-nutrition request to the deployed model.
  • Compare the model's claims with visible evidence.
  • Turn the result into a safety rule for FoodLens.
Test an exact-nutrition request

A food description asks the model to identify visible patterns. An exact-nutrition request asks it to commit to measurements from the same image.

  • Keep the same meal photo loaded in the Meal photo field.
  • Select the current text in the Prompt field.
  • Replace the selected text with Give the exact calories, protein, carbohydrates, and fat in this meal from the photo alone..
  • Predict whether the model will provide exact values or push back on the request.
  • Click Describe food.

You'll see one of two revealing outcomes. The answer either gives exact numbers without enough support from the photo or states that those exact values cannot be known.

This shortfall is intentional

A confident numerical answer exposes unsupported precision. A refusal exposes the information the image is missing. Both outcomes prove that the request reaches beyond visible evidence.

That is a useful catch. You have located the confidence problem that FoodLens needs to handle.

No response from the model?

  • Confirm that the meal photo is still visible in the Meal photo field.
  • Confirm that the full exact-nutrition request remains in the Prompt field.
  • Submit the request again after the current attempt finishes.

Help me troubleshoot a FoodLens request that is not returning a model response.

Audit the answer

This comparison checks the answer's grounding. Each claim should trace back to visible evidence or carry a clear assumption label.

  • Read the response one claim at a time.
  • Compare every exact quantity with what the photo visibly establishes.
  • Identify any ingredient that could be hidden from view.
  • Identify any cooking method that the image cannot verify.
  • Identify any portion size or mass supplied by the model.

What counts as unsupported?

  • Exact calories or macronutrients require verified ingredients plus portion amounts.
  • Hidden ingredients cannot be confirmed from appearance.
  • Cooking methods can remain uncertain when several methods look similar.
  • Visible plate size does not establish portion mass.
Turn the failure into a design rule

The evidence gaps become a product rule for every future FoodLens response. The rule keeps useful guidance separate from unsupported certainty.

FoodLens's design rule

  • Treat every numerical nutrition value as a rough estimate.
  • Show every ingredient or portion assumption to the visitor.
  • State uncertainty explicitly.
  • State the FoodLens rule in your own words using the checkpoint below.
  • Predict which claim in the answer conflicts most clearly with FoodLens's new rule.
  • Return to the response at the bottom of the App tab.
  • Check the response for unsupported precision or an explicit limit on exact values.

You should see exact nutrition presented with unsupported precision or a statement that exact calories plus macros cannot be known from the photo alone. Either outcome confirms that the pixels do not establish verified ingredients, cooking methods, or portion mass.

The confidence gap is now visible. Next, you'll turn your rule into FoodLens's cautious meal-coach behavior.

Add Cautious Meal-Coach Instructions

Your exact-nutrition test exposed the core FoodLens problem. A meal photo cannot verify hidden ingredients, preparation methods, or portion mass.

You will now give Qwen3-VL dietary preferences plus portion context. A structured Gradio interface will turn that context into guidance that labels evidence, estimates, assumptions, and unknowns.

In this step, get ready to:
  • Add a cautious meal prompt with six required report sections.
  • Connect meal photos, dietary preferences, and portion context to one analysis pipeline.
  • Rebuild FoodLens and compare its structured report with the unsupported result from the previous step.
Add the cautious analysis pipeline

The new prompt grounding rules tell the model which claims the photo can support. They also define how numerical estimates and safety limits must appear.

Why Use Structured Prompting?

FoodLens provides model-generated meal guidance without an authoritative nutrition database. Structured prompting keeps each response tied to visible evidence plus visitor-supplied context.

The six report headings make unsupported claims easier to spot. Clear sections turn uncertainty into part of the result.

  • Switch back to the Files tab in your FoodLens Space.
  • Select app.py from the repository file list.
  • Activate the repository file editor using its edit control.
  • Select the full-code tab below.
  • Replace the current contents of app.py with the complete file shown there.

✔️ Awesome, I've got everything!

Use this tab after comparing your file. Confirm that every new function and interface component matches the complete version.

ⓧ I'd like to double check the full code

import gradio as gr
import spaces
from transformers import AutoProcessor, Qwen3VLForConditionalGeneration
from qwen_vl_utils import process_vision_info

MODEL_ID = "Qwen/Qwen3-VL-2B-Instruct"

processor = AutoProcessor.from_pretrained(MODEL_ID)
model = Qwen3VLForConditionalGeneration.from_pretrained(
    MODEL_ID,
    dtype="auto",
)
model.to("cuda")


@spaces.GPU(duration=60)
def run_model(image, prompt, max_new_tokens=512):
    if image is None:
        return "Please upload or capture a meal photo."

    messages = [
        {
            "role": "user",
            "content": [
                {"type": "image", "image": image},
                {"type": "text", "text": prompt},
            ],
        }
    ]

    text = processor.apply_chat_template(
        messages,
        tokenize=False,
        add_generation_prompt=True,
    )
    image_inputs, video_inputs, video_kwargs = process_vision_info(
        messages,
        image_patch_size=16,
        return_video_kwargs=True,
        return_video_metadata=True,
    )

    if video_inputs is not None:
        video_inputs, video_metadatas = zip(*video_inputs)
        video_inputs = list(video_inputs)
        video_metadatas = list(video_metadatas)
    else:
        video_metadatas = None

    inputs = processor(
        text=text,
        images=image_inputs,
        videos=video_inputs,
        video_metadata=video_metadatas,
        return_tensors="pt",
        do_resize=False,
        **video_kwargs,
    )
    inputs = inputs.to(model.device)

    generated_ids = model.generate(**inputs, max_new_tokens=max_new_tokens)
    generated_ids_trimmed = [
        output_ids[len(input_ids) :]
        for input_ids, output_ids in zip(inputs.input_ids, generated_ids)
    ]
    return processor.batch_decode(
        generated_ids_trimmed,
        skip_special_tokens=True,
        clean_up_tokenization_spaces=False,
    )[0]


def build_meal_prompt(preferences, portion):
    preferences = (preferences or "").strip() or "No dietary preferences supplied."
    portion = (portion or "").strip() or "No portion description supplied."

    return f"""
Act as a cautious meal-photo assistant.

User dietary preferences or restrictions:
{preferences}

User-provided portion context:
{portion}

Analyze the image and return Markdown with exactly these headings:

## What I can see
List only directly visible foods and visual evidence.

## Likely meal
Name the likely dish and clearly label uncertain ingredients as likely or possible.

## Preference fit
Explain how the meal appears to fit the user's preferences. Never guarantee that an allergen is absent from an image.

## Nutrition estimate
Do not present exact calories or macronutrients as facts. If you give numbers, use broad ranges, label them as rough model-generated estimates, and state the portion and ingredient assumptions. If the portion context is missing, prefer qualitative guidance.

## Recipe adaptation
Suggest one practical adaptation that matches the user's preferences.

## Uncertainty and safety
List what cannot be verified from the photo, including hidden ingredients, preparation method, portion mass, cross-contact, and medical suitability. State that the result is not medical advice.
""".strip()


def analyze_meal(image, preferences, portion):
    prompt = build_meal_prompt(preferences, portion)
    return run_model(image, prompt)


with gr.Blocks(title="FoodLens") as demo:
    gr.Markdown(
        "# FoodLens\n"
        "Upload a meal photo and add text context. Results are estimates, not medical advice."
    )
    image_input = gr.Image(
        type="pil",
        sources=["upload", "webcam"],
        label="Meal photo",
    )
    preferences_input = gr.Textbox(
        label="Dietary preferences or restrictions",
        placeholder="Example: vegetarian, high protein, no peanuts",
        lines=2,
    )
    portion_input = gr.Textbox(
        label="Portion context",
        placeholder="Example: one medium bowl, about two cups",
        lines=2,
    )
    analyze_button = gr.Button("Analyze meal", variant="primary")
    output = gr.Markdown("Your FoodLens analysis will appear here.")

    analyze_button.click(
        fn=analyze_meal,
        inputs=[image_input, preferences_input, portion_input],
        outputs=output,
    )


if __name__ == "__main__":
    demo.launch()

How Is the Finished App Organized?

  • The build_meal_prompt() function converts preferences plus portion context into six required report sections.
  • The analyze_meal() function passes that grounded prompt to run_model().
  • The image_input, preferences_input, and portion_input components collect the visitor's evidence and context.
  • The analyze_button sends all three values to the analysis pipeline. The returned Markdown appears in output.
Rebuild the public interface

Committing the completed file triggers a fresh Space build. The rebuilt page should replace the temporary prompt box with dedicated preference and portion inputs.

  • Commit the updated app.py file.
  • Keep the Space page open during the automatic rebuild.
  • Return to the App tab after the build completes.

A rebuild can take several minutes while the Space reloads the cached model. The wait means your new interface is being deployed.

You'll see a Meal photo input, a Dietary preferences or restrictions field, a Portion context field, and an Analyze meal button.

Rebuild Not Completing?

  • Compare app.py with the full-file reference. A missing bracket or mismatched indentation can stop the build.
  • Confirm that requirements.txt still contains the three pinned packages from the earlier setup.

Help me diagnose why my FoodLens Space does not rebuild after updating app.py.

Compare the cautious report

The final check uses the same meal photo that exposed the confidence problem. Added visitor context lets you see whether the new response makes its evidence boundaries visible.

  • Keep the same meal photo selected in the Meal photo input.
  • Enter vegetarian, high protein, no peanuts in the Dietary preferences or restrictions field.
  • Enter one medium bowl, about two cups in the Portion context field.

Before you submit the meal, do you expect the response to separate visible evidence from assumptions?

  • Click Analyze meal to run the updated analysis.

You'll see sections named What I can see, Likely meal, Preference fit, Nutrition estimate, Recipe adaptation, and Uncertainty and safety.

  • Compare the structured report with the exact-nutrition answer from the previous step.
  • Check the Nutrition estimate section for rough ranges plus visible assumptions.
  • Check the Uncertainty and safety section for limits involving hidden ingredients, preparation, portion mass, cross-contact, and medical suitability.

You closed the core safety gap. FoodLens now communicates what the model can see plus what it still cannot verify.

Missing the Structured Sections?

  • Confirm that analyze_button.click() uses analyze_meal as its callback.
  • Confirm that analyze_meal() calls build_meal_prompt() before calling run_model().

Help me find why my FoodLens result does not include all six Markdown sections.

Your uncertainty-aware meal coach is ready. Next, you'll test the public URL from a signed-out visitor's perspective.

Test FoodLens as a Public User

Your Qwen3-VL pipeline now turns a meal photo plus visitor context into a structured report. The cautious meal coach is live on Hugging Face Spaces.

A successful public deployment lets a signed-out visitor run FoodLens without repository access. You will reproduce that visitor experience in a private browser window.

You will also prepare a tester for ZeroGPU queueing plus daily quota limits. Clear expectations make the shared app easier for other people to use.

In this step, get ready to:
  • Confirm that FoodLens is publicly reachable from a signed-out browser.
  • Run a complete meal analysis from the signed-out page.
  • Explain ZeroGPU visitor limits to a tester.
Confirm public access

Public visibility controls whether visitors can reach FoodLens. Its page URL is the link you can share outside your account.

  • Select Settings in the Space navigation.
  • Locate the Visibility setting.
  • Confirm that it displays Public.
  • Return to App in the Space navigation.
  • Copy the shareable page URL from your browser's address bar.
  • Record the URL here: your FoodLens public URL.

What Does Public Access Prove?

Public visibility allows people outside your account to reach the Space. The private-window test checks that the app works without your signed-in session.

  • Open a private browsing window from your browser's main menu.
  • Paste your FoodLens public URL into the address bar.
  • Load the page by pressing Enter.
  • Confirm that the page does not show your signed-in Hugging Face profile.

You should see the FoodLens heading on the public page. You should also see the three visitor inputs plus the Analyze meal button.

Still Signed In?

  • Close every private browser window.
  • Open a new private window from your browser's main menu.
  • Load your FoodLens public URL again.

If your account still appears, help me verify a signed-out Hugging Face Space test.

Run a signed-out meal analysis

A public test still needs a privacy-safe input. The safest photo shows only the meal.

  • Upload a meal photo with no private information through Meal photo.
  • Enter vegetarian, high protein, no peanuts in Dietary preferences or restrictions.
  • Enter one medium bowl, about two cups in Portion context.

Before you submit it, think about whether signing out changes the report structure.

  • Click Analyze meal.

The request may enter a ZeroGPU queue before processing starts. That wait reflects shared GPU capacity.

  • Keep the private window open until the request completes.

What Should I See?

When the request finishes, you should see a Markdown report with the same six safety sections that appeared for you as the owner.

  • The What I can see section identifies visible evidence.
  • The Likely meal section labels the probable dish.
  • The Preference fit section discusses your dietary request.
  • The Nutrition estimate section presents cautious estimates.
  • The Recipe adaptation section suggests a practical change.
  • The Uncertainty and safety section names important limits.

Report Not Returning?

  • Leave the private page open while the request remains queued.
  • Check whether the page reports that your daily ZeroGPU allowance is exhausted.
  • Retry later if shared capacity is unavailable.

If the request still does not finish, help me troubleshoot my queued ZeroGPU request.

That is the public proof point. FoodLens now accepts a signed-out visitor's input without exposing repository access.

Prepare a public tester

Shared GPU capacity can place a visitor's request in a queue. Daily access also depends on whether that visitor is signed in.

  • Send your FoodLens public URL to one tester.
  • Ask the tester to open the link.
  • Tell the tester that ZeroGPU requests can queue.
  • Tell the tester that unauthenticated visitors receive 2 GPU-minutes daily.
  • Tell the tester that free signed-in visitors receive 5 GPU-minutes daily.
  • Ask the tester to confirm that the FoodLens heading appears.

Their confirmation proves that your shareable URL reaches the deployed app from another visitor's browser.

FoodLens is now proven as a public meal coach. Other people can open your URL and run the same structured analysis you tested.

Secret mission

Add an Evidence Audit

Run each meal report through a second Qwen3-VL pass that challenges the first answer against the original photo. The audit separates visible evidence from inference and unsupported claims before a visitor relies on the result.

Clean Up Your Resources

Clean Up Your Resources

FoodLens is running as a public Hugging Face Space with a ZeroGPU-hosted Gradio app. Keeping the Space public costs $0 within the eligible ZeroGPU limits.

Resources you used:

  • Your public FoodLens Hugging Face Space repository with its deployed app.

Keep everything running

No action needed. Choose this if you want visitors to keep using FoodLens through its shareable URL.

  • Leave the FoodLens Space public.
  • Keep the shareable URL for future demonstrations.
  • Tell visitors that each analysis uses two inference calls from their available quota.

Pause - I'll come back to this later

Pause the Space to make FoodLens unavailable while preserving its repository. Only you can restart the paused Space.

  • Return to your FoodLens Space.
  • Select the Settings tab.
  • Select Pause.
  • Reload the shareable FoodLens URL.

You'll see that FoodLens no longer accepts meal submissions. Your repository remains available for a later restart.

Delete - I don't want to use this again

Deleting the Space removes the public app with its repository. This action is irreversible, so choose it only when you no longer need FoodLens.

  • Return to your FoodLens Space repository.
  • Select the Settings tab to access the repository controls.
  • Use the repository deletion control to delete the FoodLens Space repository.
  • Confirm the deletion when prompted.
  • Reload the shareable FoodLens URL.

You'll see that the public FoodLens app is no longer available. The stable page URL can no longer accept visitor requests.

Nice Work!

Nice Work!

Outstanding work! FoodLens is now live on Hugging Face Spaces as a public meal coach with uncertainty-aware results.

What you learned:

  • Published a public Qwen3-VL image-and-text pipeline using ZeroGPU. Visitors can run it from a shareable URL without access to your computer.
  • Built a Gradio interface that accepts a meal photo. It combines the image with dietary preferences and portion context.
  • Exposed the limits of image-only nutrition analysis with an exact-nutrition test. You converted that shortfall into uncertainty-aware guidance that labels estimates, assumptions, and safety limits.
  • Secret Mission: Added a second-pass evidence audit that challenges unsupported claims. The audit classifies important details as directly visible, inferred, or unknown or unsupported.

Ready to quiz yourself?