From Notebook to App: Wrap a Model in a UI Anyone Can Use
Build an AI app in PyTorch by wrapping a pretrained model in a Gradio app — a beginner-friendly capstone that turns a model into a real, shareable little tool.
A model running in a notebook is a private victory. You typed a cell, got a result, felt clever — and nobody else can see it. This chapter closes the gap between you ran a model and you built a thing people can use. We’re going to take a pretrained model, wrap it in a plain Python function, and put a little web UI on it with Gradio — running right inside the notebook, with a public link you can text to a friend.
This is Part I’s capstone — the payoff for everything from your first tensor to your first model run. By the end you’ll have a real, shareable image-classification app, and you’ll see how the pieces of the last nine chapters snap together.
▶ Open the companion notebook in Colab
Run against: PyTorch 2.2.2, torchvision 0.17.2 and transformers 4.57.6 on CPU (macOS). The diffusers aside at the end was not run; see the note there.
A model is already a function
Here’s the reframe that makes the whole thing easy: a model is just a function. Input goes in, output comes out. That’s been the shape of everything in Part I: load a pretrained thing, hand it an input, read the result. An app is nothing more than that same function with a UI bolted on. The model does the hard part. Our job is just to give it a way in and a way out.
So we start by writing the function. Let’s use an image classifier — point it at a photo, get back the most likely labels. We’ll lean on pipeline() from chapter 7 because it does the preprocessing for us, and reach for ResNet-50 — the same model you ran by hand in chapter 4, this time Hugging Face’s port of it (which, as chapter 4 mentioned, is sharper on the Labrador: 88% to torchvision’s 12.6%):
from transformers import pipeline
classifier = pipeline("image-classification", model="microsoft/resnet-50")
def classify(img):
results = classifier(img)
# Turn the list of {label, score} into the {label: score} a UI wants.
return {r["label"]: r["score"] for r in results}
That’s it. classify takes an image and returns a dictionary of labels and confidence scores. You could call it in a cell and print the result, but a dictionary isn’t something you hand a non-engineer. It needs a face.
Putting a UI on it with Gradio
Gradio is a library that builds a web interface around a Python function. You tell it what kind of input your function takes and what kind of output it returns, and it generates the page — file upload, buttons, result display, all of it. No HTML, no JavaScript, no front-end anything.
import gradio as gr
demo = gr.Interface(
fn=classify,
inputs=gr.Image(type="pil"),
outputs=gr.Label(num_top_classes=3),
title="What's in this picture?",
description="Upload an image and the model guesses what it sees.",
)
demo.launch(share=True)
Three arguments carry the weight. fn is your function — the model. inputs=gr.Image(type="pil") tells Gradio to render an image uploader and hand your function a PIL image (which is exactly what the pipeline wants — the types line up on purpose). outputs=gr.Label(num_top_classes=3) says “the function returns label scores; draw them as a tidy top-3 bar chart.”
Run that cell and a working app appears inside the notebook: drag in a photo, watch the bars fill in. Everything you learned about inputs and outputs in chapter 6 is quietly doing its job here — the uploader produces a PIL image, the pipeline preprocesses it, the model scores it, and Gradio renders the result. You just don’t have to think about any of it.
The public link

The share link tunnels from a friend’s browser back to the Gradio app in your notebook.
Notice share=True. Without it, the app only exists in your notebook. With it, Gradio spins up a temporary public URL — something like https://a1b2c3.gradio.live — that tunnels back to your running notebook. Send it to someone and they can use your app in their browser, no install, no Colab account.
That link is the punchline of Part I. Ten chapters ago you didn’t know what a tensor was; now you have a URL you can paste into a chat and say “here, try this.” The link lives as long as your notebook is running (Gradio says up to a week, best effort), then it’s gone, which makes it perfect for a quick demo and useless for production. But for “let me show you what I built,” it’s exactly the right tool, and there’s nothing else to install or configure to get it.
A taste of another modality: making images
You’ve been running models that read and classify. To hint at how much else is one from_pretrained away, let’s flip to a model that creates — text-to-image generation, via Hugging Face’s diffusers library. Same muscle memory, brand-new superpower. One thing first, since chapter 8 taught you to look: stabilityai/sdxl-turbo ships under Stability’s own community licence, which is free for research and small-scale use and not a permissive licence. Fine for a notebook; read it before it goes anywhere near a product.
Not run. This aside needs a CUDA GPU and several gigabytes of weights; it was written against the
diffusers0.40 API (which acceptsdtype=and treats the oldertorch_dtype=as a deprecated alias) and not executed.
from diffusers import AutoPipelineForText2Image
import torch
pipe = AutoPipelineForText2Image.from_pretrained(
"stabilityai/sdxl-turbo",
dtype=torch.float16,
).to("cuda")
image = pipe("a watercolor fox in a misty forest",
num_inference_steps=2,
guidance_scale=0.0).images[0]
image # in a notebook, this just displays the picture
Read it as the same pattern you already know: from_pretrained loads the model, you hand it an input (a text prompt this time), you read the output (a PIL image). sdxl-turbo is a “turbo” model tuned to produce a picture in just a couple of steps, which is why we set num_inference_steps=2 and guidance_scale=0.0 — those are simply its recommended fast-mode settings. A regular image model would want more steps and a higher guidance value, and would take correspondingly longer. The point isn’t this one model; it’s that text, audio, and image models all answer to the same few lines.
And here’s the lovely part: wrap that in a gr.Interface with inputs="text" and outputs="image", and you’ve built an image generator with a UI. The recipe never changed — only the model did.
Gotchas
A few things that catch people on their first app:
- The share link is temporary, not a deployment.
share=Truegives you a demo URL that dies when your notebook stops (and within about a week regardless). For something permanent, look at Hugging Face Spaces — it hosts Gradio apps for free. The share link is for showing, not shipping. diffusersreally wants a GPU. Image generation is heavy. On a CPU it can take minutes per image, if it finishes at all. Flip on the GPU (Runtime → Change runtime type → GPU) before you run thediffuserscell, and keep the.to("cuda")— on a CPU-only runtime that line will error, which is your sign the GPU isn’t on.- Match the input type to what the model expects.
gr.Image(type="pil")hands your function a PIL image;type="numpy"hands it a NumPy array instead. Theimage-classificationpipeline is happy with PIL, so we used PIL. If your function chokes on its input, this is the first thing to check. - The first run is slow, then it’s fast. The first call downloads model weights (sometimes gigabytes). After that they’re cached for the session, and the app responds quickly. Don’t mistake the one-time download for the app being slow.
float16is a GPU thing.dtype=torch.float16halves memory and speeds things up, but half-precision is a GPU trick (see chapter 21). On CPU, drop it and let the model run in the default precision.- Launching twice. Re-running
launch()in the same session can complain that a port is in use. If it grumbles, restart the runtime (Runtime → Restart) and run from the top — the cleanest reset there is.
What’s next
That’s Part I: a model run end to end, read at both ends, found on the Hub, debugged, and now wrapped in an app with a link you can share. It is a complete skill — load a model, feed it the right input, read its output, put a face on it — and if running models is all you came for, you can skip straight to Part III and start on text.
Part II is for the other itch. You have been treating the model as a sealed box that happens to work, and it has worked. The next five chapters open the box, not by building one but by reading one — ResNet, then a language model — so that a printout of layers and a wall of parameter names stop being noise. It is short, it is optional, and it pays off the first time a shape error names a layer you recognise.
Next: What a Model Is Made Of: Build One from a Single Layer Up.
Comments