Danh sách bài viết

Bài 9: Gradio — demo UI trong vài dòng code

Tạo web UI cho model AI bằng Python thuần với Gradio 4.x — không cần HTML, CSS, JavaScript. Bài này đi qua ba cách build UI (Interface, ChatInterface, Blocks), các launch option, tích hợp với FastAPI và những pitfall thường gặp.

27/05/2026
0 lượt xem
1

Mục tiêu bài học

Sau bài này bạn sẽ:

  • ✅ Phân biệt được Gradio phù hợp với trường hợp nào
  • ✅ Dùng gr.Interface, gr.ChatInterface, gr.Blocks đúng mục đích
  • ✅ Biết các option của launch() và giới hạn của chúng
  • ✅ Mount Gradio vào app FastAPI hiện có
  • ✅ Tránh được các pitfall phổ biến (model load, queue, type mismatch)
2

Khi nào dùng Gradio

Gradio phù hợp nhất khi:

  • Demo cho người không-tech — PM, designer, recruiter cần thử model mà không muốn gọi API thủ công.
  • Internal QA tool — team QA cần giao diện để test output model nhanh, không cần frontend engineer làm UI riêng.
  • Notebook → public demo — convert Jupyter notebook thành app chạy trên Hugging Face Spaces (bài 12 sẽ đề cập deploy).

Gradio không phải lựa chọn phù hợp khi:

  • Cần nhiều trang (multi-page navigation) với routing phức tạp — lúc đó dùng Streamlit, Dash, hoặc React.
  • Cần custom CSS/JS sâu cho UI — Gradio có khả năng custom nhất định nhưng hạn chế hơn framework frontend.
  • App production phục vụ hàng nghìn user đồng thời — Gradio thiếu các tính năng scale của web framework chuyên dụng.
3

Cài đặt

pip install gradio
# Kiểm tra version (current major: 4.x)
python -c "import gradio; print(gradio.__version__)"

Gradio 4.x yêu cầu Python ≥ 3.8. Nếu cần isolate môi trường:

python -m venv .venv
source .venv/bin/activate   # Linux/macOS
# hoặc
.venv\Scripts\activate      # Windows

pip install gradio

Gradio 4 đi kèm server uvicorn nội bộ — không cần cài thêm ASGI server để chạy local.

4

Hello world — 3 dòng code

import gradio as gr

def greet(name: str) -> str:
    return f"Hello {name}!"

gr.Interface(fn=greet, inputs="text", outputs="text").launch()

Chạy file → Gradio tự mở browser tại http://127.0.0.1:7860. Terminal in:

Running on local URL:  http://127.0.0.1:7860

gr.Interface nhận 3 tham số bắt buộc:

  • fn — hàm Python xử lý logic
  • inputs — kiểu input hoặc component Gradio
  • outputs — kiểu output hoặc component Gradio

String shorthand như "text", "image", "audio" tương đương với gr.Textbox(), gr.Image(), gr.Audio(). Khi cần cấu hình chi tiết hơn (placeholder, label, type) thì dùng component object.

5

Ba cách build UI

Gradio 4 cung cấp ba mức abstraction:

API Mục đích Khi nào dùng
gr.Interface Shortcut cho 1 function, layout cố định (input bên trái, output bên phải) Demo đơn giản 1 input → 1 output (hoặc nhiều input/output nhưng cùng 1 lần call)
gr.ChatInterface Shortcut cho chatbot — tự tạo message list, nút Retry, Clear, history Bất kỳ chatbot nào nhận message + history
gr.Blocks API low-level, tự khai báo layout (Row, Column, Tab) và event handler Layout phức tạp, nhiều function, nhiều tab, state session
6

Ví dụ 1 — Image classifier với gr.Interface

Demo image classifier trả top-5 class với confidence score:

import gradio as gr
from PIL import Image
import torch
from torchvision import models, transforms

# Load model 1 lần khi module được import — KHÔNG load trong callback
model = models.resnet50(weights=models.ResNet50_Weights.DEFAULT)
model.eval()

# ImageNet class labels (rút gọn — thực tế load từ file)
IMAGENET_LABELS = {0: "tench", 1: "goldfish"}  # ... 1000 classes

preprocess = transforms.Compose([
    transforms.Resize(256),
    transforms.CenterCrop(224),
    transforms.ToTensor(),
    transforms.Normalize(mean=[0.485, 0.456, 0.406],
                         std=[0.229, 0.224, 0.225]),
])

def classify(img: Image.Image) -> dict[str, float]:
    """Nhận PIL.Image, trả dict {label: confidence}."""
    tensor = preprocess(img).unsqueeze(0)  # (1, 3, 224, 224)
    with torch.no_grad():
        logits = model(tensor)
    probs = torch.softmax(logits[0], dim=0)
    top5 = probs.topk(5)
    return {
        IMAGENET_LABELS.get(idx.item(), str(idx.item())): round(prob.item(), 4)
        for prob, idx in zip(top5.values, top5.indices)
    }

demo = gr.Interface(
    fn=classify,
    inputs=gr.Image(type="pil"),       # trả PIL.Image vào hàm, không phải numpy
    outputs=gr.Label(num_top_classes=5),
    title="ResNet-50 Image Classifier",
    examples=["cat.jpg", "dog.jpg"],   # optional: ảnh mẫu
)

demo.launch()

Lưu ý quan trọng: gr.Image(type="pil") trả PIL.Image vào hàm. Nếu bỏ tham số type (mặc định là "numpy") mà hàm lại expect PIL.Image thì sẽ báo lỗi runtime. Xem pitfall ở mục 11.

gr.Label nhận dict[str, float] và tự vẽ bar chart xác suất — không cần xử lý thêm.

7

Ví dụ 2 — Chatbot với gr.ChatInterface

Chatbot cơ bản

import gradio as gr

def chat(message: str, history: list) -> str:
    """
    history: list of [user_msg, assistant_msg] pairs
    Gradio truyền tự động, không cần quản lý thủ công.
    """
    # Thay bằng LLM call thực tế
    return f"Bạn vừa nói: {message}"

gr.ChatInterface(fn=chat).launch()

gr.ChatInterface tự render message list, nút Retry (gửi lại message cuối), Clear (xóa history) và Undo (xóa lượt cuối). Không cần code gì thêm.

Streaming response

Nếu LLM của bạn trả từng token (như OpenAI streaming), dùng yield thay vì return:

import gradio as gr
from openai import OpenAI

client = OpenAI()  # đọc OPENAI_API_KEY từ env

def chat_stream(message: str, history: list):
    messages = []
    for user_msg, assistant_msg in history:
        messages.append({"role": "user", "content": user_msg})
        messages.append({"role": "assistant", "content": assistant_msg})
    messages.append({"role": "user", "content": message})

    stream = client.chat.completions.create(
        model="gpt-4o-mini",
        messages=messages,
        stream=True,
    )

    partial = ""
    for chunk in stream:
        delta = chunk.choices[0].delta.content or ""
        partial += delta
        yield partial  # Gradio render từng phần, user thấy text xuất hiện dần

gr.ChatInterface(fn=chat_stream).launch()

Gradio phát hiện generator (yield) và tự bật streaming — không cần cấu hình thêm. Token xuất hiện dần trên giao diện giống ChatGPT.

8

Ví dụ 3 — gr.Blocks với layout phức tạp

gr.Blocks dùng context manager để khai báo UI dưới dạng code:

import gradio as gr

def summarize(text: str, max_words: int) -> str:
    words = text.split()[:max_words]
    return " ".join(words) + ("..." if len(text.split()) > max_words else "")

def word_count(text: str) -> str:
    return f"{len(text.split())} từ"

with gr.Blocks(title="Text Tools") as demo:
    gr.Markdown("## Text Tools")

    with gr.Tab("Summarize"):
        with gr.Row():
            with gr.Column():
                input_text = gr.Textbox(
                    label="Input text",
                    lines=8,
                    placeholder="Paste văn bản vào đây...",
                )
                max_words = gr.Slider(
                    minimum=10,
                    maximum=200,
                    value=50,
                    step=10,
                    label="Max words",
                )
                btn_summarize = gr.Button("Summarize", variant="primary")
            with gr.Column():
                output_text = gr.Textbox(label="Output", lines=8)

        btn_summarize.click(
            fn=summarize,
            inputs=[input_text, max_words],
            outputs=output_text,
        )

    with gr.Tab("Word Count"):
        text_wc = gr.Textbox(label="Input", lines=4)
        count_output = gr.Textbox(label="Result")
        # change event: tự động chạy khi text thay đổi (không cần nút)
        text_wc.change(fn=word_count, inputs=text_wc, outputs=count_output)

demo.launch()

gr.State — giữ data giữa các request

gr.State lưu data theo session (mỗi tab browser là 1 session riêng):

import gradio as gr

def add_item(item: str, cart: list) -> tuple[list, str]:
    """Thêm item vào cart của session này."""
    cart = cart + [item]   # không mutate list gốc
    return cart, f"Cart: {', '.join(cart)}"

with gr.Blocks() as demo:
    cart_state = gr.State([])   # giá trị khởi tạo: list rỗng

    item_input = gr.Textbox(label="Item")
    add_btn = gr.Button("Add to cart")
    cart_display = gr.Textbox(label="Cart summary")

    add_btn.click(
        fn=add_item,
        inputs=[item_input, cart_state],
        outputs=[cart_state, cart_display],
    )

demo.launch()

gr.State không hiển thị ra UI — chỉ lưu giá trị. Khi function trả về giá trị mới cho cart_state, Gradio tự cập nhật state cho session đó.

9

launch() options

Các option thường dùng

demo.launch(
    server_name="0.0.0.0",   # expose ra LAN / container (mặc định: "127.0.0.1")
    server_port=7860,         # port (mặc định: 7860)
    share=False,              # xem giải thích bên dưới
    auth=("admin", "secret"), # basic auth — dạng tuple hoặc callback function
    ssl_certfile="cert.pem",  # HTTPS
    ssl_keyfile="key.pem",
)

share=True

share=True tạo một đường dẫn public dạng https://xxxx.gradio.live qua Gradio tunnel. Dùng được cho demo nhanh, nhưng có giới hạn quan trọng:

  • Link tồn tại 72 giờ rồi hết hiệu lực.
  • URL ngẫu nhiên — không kiểm soát được domain.
  • Traffic đi qua server của Gradio — không dùng cho dữ liệu nhạy cảm.
  • Không phù hợp production. Nếu cần link permanent, dùng Hugging Face Spaces (bài 12).

auth callback function

Nếu cần logic auth phức tạp hơn username/password đơn giản:

def check_auth(username: str, password: str) -> bool:
    # Ví dụ: kiểm tra trong database
    valid_users = {"alice": "pass1", "bob": "pass2"}
    return valid_users.get(username) == password

demo.launch(auth=check_auth)

queue()

Gradio 4 bật queue mặc định. Với Gradio 3 hoặc khi cần cấu hình chi tiết hơn:

demo.queue(
    max_size=20,           # tối đa 20 request trong hàng đợi
    default_concurrency_limit=2,  # xử lý 2 request song song
).launch()
10

Gradio + FastAPI cùng app

Gradio cung cấp hàm mount_gradio_app để mount vào FastAPI:

import gradio as gr
from fastapi import FastAPI
import uvicorn

app = FastAPI()

# --- FastAPI routes cho production (JSON API) ---
@app.get("/health")
def health():
    return {"status": "ok"}

@app.post("/predict")
def predict(payload: dict):
    # ... inference logic
    return {"result": "..."}

# --- Gradio demo cho team QA ---
def run_model(text: str) -> str:
    # Gọi lại cùng logic inference
    return f"Prediction: {text[::-1]}"

gradio_demo = gr.Interface(fn=run_model, inputs="text", outputs="text")

# Mount Gradio vào path /gradio
app = gr.mount_gradio_app(app, gradio_demo, path="/gradio")

if __name__ == "__main__":
    uvicorn.run(app, host="0.0.0.0", port=8000)

Sau khi chạy:

  • http://localhost:8000/predict — JSON API cho production
  • http://localhost:8000/gradio — Gradio UI cho team test

Pattern này hữu ích khi bạn đã có FastAPI app (từ Module 1) và muốn thêm UI test mà không cần spin up server riêng.

11

Common pitfalls

1. Load model trong callback — chậm mỗi request

Sai:

def classify(img):
    model = load_model("weights.pt")  # Load lại mỗi lần gọi!
    return model(img)

Đúng: Load model ở module level (global) hoặc trong if __name__ == "__main__" trước khi launch():

model = load_model("weights.pt")  # Load 1 lần khi file được import

def classify(img):
    return model(img)  # Dùng model đã load sẵn

2. share=True để làm link permanent

Link .gradio.live hết hạn sau 72h. Nếu cần link dài hạn, deploy lên Hugging Face Spaces hoặc host server riêng (xem bài 12).

3. Type input/output không khớp component

gr.Image có hai chế độ:

  • gr.Image(type="pil") → hàm nhận PIL.Image
  • gr.Image(type="numpy") (mặc định) → hàm nhận numpy.ndarray shape (H, W, 3)
  • gr.Image(type="filepath") → hàm nhận đường dẫn file string

Nhầm type dẫn đến AttributeError hoặc kết quả sai im lặng. Luôn chỉ định rõ type thay vì dùng mặc định.

4. Quên queue() với nhiều user (Gradio 3)

Gradio 3 mặc định không có queue — nếu nhiều request đến cùng lúc, các request sau block chờ request trước xong. Gradio 4 đã bật queue mặc định. Nếu dùng Gradio 3 hoặc cần cấu hình cụ thể, gọi demo.queue().launch().

5. Mutate list state trực tiếp

Khi dùng gr.State lưu list hoặc dict, không nên mutate object gốc:

# Sai: mutate in-place
def add(item, cart):
    cart.append(item)   # Gradio không detect thay đổi đáng tin cậy
    return cart

# Đúng: tạo object mới
def add(item, cart):
    return cart + [item]
12

Tóm tắt

  • ✅ Gradio phù hợp cho demo nội bộ, QA tool, Notebook → app — không phù hợp multi-page production app
  • ✅ Ba API chính: gr.Interface (đơn giản), gr.ChatInterface (chatbot), gr.Blocks (layout tự do)
  • gr.Image(type="pil") phải khớp với type hàm Python expect
  • ✅ Streaming: dùng yield trong hàm callback — Gradio tự nhận diện
  • share=True cho demo 72h, không phải permanent hosting
  • mount_gradio_app tích hợp Gradio vào FastAPI hiện có
  • ✅ Load model ở module level, không load trong callback