Mục lục
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)
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.
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.
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ý logicinputs— kiểu input hoặc component Gradiooutputs— 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.
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 |
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.
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.
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 đó.
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()
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 productionhttp://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.
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ậnPIL.Imagegr.Image(type="numpy")(mặc định) → hàm nhậnnumpy.ndarrayshape(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]
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
yieldtrong hàm callback — Gradio tự nhận diện - ✅
share=Truecho demo 72h, không phải permanent hosting - ✅
mount_gradio_apptích hợp Gradio vào FastAPI hiện có - ✅ Load model ở module level, không load trong callback
