🌐 Gradio: a web page from a Python function · Gradio: หน้าเว็บจากฟังก์ชัน Python · Gradio:用一个 Python 函数做网页

Three lines gives you a page in a browser. No HTML, no CSS, no JavaScript. Build a chat that streams. Add controls that hand you real numbers, and a login. Then five challenges, ending with a model that calls your code itself. · สามบรรทัดก็ได้หน้าเว็บในเบราว์เซอร์ ไม่ต้องมี HTML CSS หรือ JavaScript สร้างแชตที่ไหลทีละคำ ตัวควบคุมที่ส่งตัวเลขจริงมาให้ และหน้าล็อกอิน แล้วตามด้วยโจทย์ห้าข้อ จบด้วยโมเดลที่เรียกโค้ดของคุณเอง · 三行就有一个浏览器里的页面。不用 HTML、CSS 或 JavaScript。做一个会流式输出的聊天、几个直接给你数字的控件,和一个登录页。然后五道题,最后是一个会自己调用你代码的模型。

🇬🇧 English

Gradio turns a Python function into a web page. You write the function. It writes the page.

No HTML, no CSS, no JavaScript. Three lines gives you something you can open in a browser.

It is how most people show an AI model to somebody else.

🇹🇭 ไทย

Gradio เปลี่ยนฟังก์ชัน Python ให้กลายเป็นหน้าเว็บ คุณเขียนฟังก์ชัน มันเขียนหน้าเว็บให้

ไม่ต้องมี HTML ไม่ต้องมี CSS ไม่ต้องมี JavaScript สามบรรทัดก็ได้ของที่เปิดในเบราว์เซอร์ได้แล้ว

นี่คือวิธีที่คนส่วนใหญ่ใช้โชว์โมเดล AI ให้คนอื่นดู

🇨🇳 中文

Gradio 把一个 Python 函数变成一个网页。你写函数,它写页面。

不用 HTML,不用 CSS,不用 JavaScript。三行就有一个能在浏览器里打开的东西。

这是大多数人把 AI 模型拿给别人看的办法。

tkintertkintertkinterGradioGradioGradio
Where it opensเปิดที่ไหน在哪打开a window on this computerหน้าต่างบนเครื่องนี้这台电脑上的一个窗口a page in a browserหน้าเว็บในเบราว์เซอร์浏览器里的一个页面
Who can use itใครใช้ได้谁能用whoever sits hereคนที่นั่งอยู่ตรงนี้坐在这儿的人anyone you give the addressใครก็ได้ที่คุณให้ที่อยู่ไป任何你把地址给他的人
You write the layoutคุณเขียนเลย์เอาต์布局由你写yes, widget by widgetใช่ ทีละวิดเจ็ต是的,一个控件一个控件only when you want toเฉพาะตอนที่คุณอยากเขียนเอง只在你想写的时候
Words appear as writtenตัวอักษรโผล่ตอนที่เขียน字边写边出现threads and a queueต้องใช้เธรดกับคิว得用线程和队列one word: yieldคำเดียว: yield一个词:yield

💻 INSTALL IT FIRST

pip install gradio
THIS STARTS A WEB SERVER ON YOUR COMPUTER Every example here opens a port and serves a page. By default only this computer can reach it. Step 6 is about letting other people in. It says what to consider first. Stop any example with Ctrl-C. ทุกตัวอย่างในหน้านี้จะเปิดพอร์ตและให้บริการหน้าเว็บ โดยค่าเริ่มต้นมีแค่เครื่องนี้ที่เข้าถึงได้ ขั้นที่ 6 พูดถึงการให้คนอื่นเข้ามา และสิ่งที่ต้องคิดก่อน หยุดตัวอย่างไหนก็ได้ด้วย Ctrl-C 这里每个例子都会开一个端口、提供一个页面。默认只有这台电脑能连上。第 6 步讲怎么让别人进来,以及之前该先想什么。用 Ctrl-C 停掉任何一个例子。

🌐 Tutorial 1 — A web page in three lines · บทที่ 1 — หน้าเว็บในสามบรรทัด · 教程 1 —— 三行一个网页

🇬🇧 English

The first one is genuinely three lines. The second says where things go, which is the only reason to write more.

🇹🇭 ไทย

อันแรกสามบรรทัดจริง ๆ อันที่สองบอกว่าอะไรอยู่ตรงไหน ซึ่งเป็นเหตุผลเดียวที่ต้องเขียนเพิ่ม

🇨🇳 中文

第一个真的就三行。第二个讲东西摆在哪,那是多写几行的唯一理由。

1 One function, one page · หนึ่งฟังก์ชัน หนึ่งหน้าเว็บ · 一个函数,一个页面

🇬🇧 English

gr.Interface reads your function and draws the page around it.

One argument in, so one box in. One value out, so one box out. You never said how wide anything is.

🇹🇭 ไทย

gr.Interface อ่านฟังก์ชันของคุณ แล้ววาดหน้าเว็บล้อมรอบมัน

รับหนึ่งค่า ก็มีหนึ่งช่องกรอก คืนหนึ่งค่า ก็มีหนึ่งช่องผลลัพธ์ คุณไม่เคยบอกเลยว่าอะไรกว้างเท่าไหร่

🇨🇳 中文

gr.Interface 读你的函数,围着它把页面画出来。

进一个参数,就一个输入框。出一个值,就一个输出框。你从没说过任何东西有多宽。

📄 NEW FILE · hello1.py

"""hello1.py — a web page with three lines and no HTML."""
import gradio as gr


def greet(name):
    """Whatever comes back from here is what the page shows."""
    return "Sawasdee, " + name + "!"


# Interface wires one function to one page. It reads the function to work out
# what to draw: one argument in, so one box in; one value out, so one box out.
#   fn      - the function to run when the button is pressed
#   inputs  - "text" is shorthand for gr.Textbox()
#   outputs - the same shorthand, for where the answer goes
demo = gr.Interface(fn=greet, inputs="text", outputs="text",
                    title="Say hello")

if __name__ == "__main__":
    # launch() starts a small web server and prints its address. Open that
    # address in a browser. Ctrl-C in the terminal stops it.
    demo.launch()
LineWhat it does · ทำอะไร · 做什么
import gradio as grEveryone writes gr. Follow them, so other people's code reads the same.ทุกคนเขียน gr ทำตามเขา โค้ดของคนอื่นจะได้อ่านเหมือนกัน所有人都写 gr。跟着写,别人的代码才读着一样。
gr.Interface(fn=greet, inputs="text", outputs="text",text is shorthand for a Textbox. The order matters, not the name.text คือคำย่อของ Textbox ลำดับสำคัญ ไม่ใช่ชื่อtext 是 Textbox 的简写。重要的是顺序,不是名字。
demo.launch()Starts a small web server and prints its address. Ctrl-C stops it.เริ่มเว็บเซิร์ฟเวอร์เล็ก ๆ แล้วพิมพ์ที่อยู่ออกมา กด Ctrl-C เพื่อหยุด启动一个小的 web 服务器并打印地址。Ctrl-C 停掉。
if __name__ == "__main__":So another file can import demo without a server starting.เพื่อให้ไฟล์อื่นอิมพอร์ต demo ได้โดยที่เซิร์ฟเวอร์ไม่เริ่มทำงาน这样别的文件可以 import demo,而不会顺手起一个服务器。

🖥️ WHAT IT PRINTED · and what the page answered

greet('Nari') -> 'Sawasdee, Nari!'
served at http://127.0.0.1:7871/   status 200, 16,305 bytes
YOUR TURN
  1. Run it. Open the address it prints.
  2. Type your name. Press Submit.
  3. Change outputs="text" to outputs="number". Read the error.
  4. Add a second argument to greet. Watch a second box appear.
  1. รันดู แล้วเปิดที่อยู่ที่มันพิมพ์ออกมา
  2. พิมพ์ชื่อคุณ แล้วกด Submit
  3. เปลี่ยน outputs="text" เป็น outputs="number" แล้วอ่านข้อผิดพลาด
  4. เพิ่มอาร์กิวเมนต์ที่สองให้ greet แล้วดูช่องที่สองโผล่มา
  1. 运行它,打开它打印的地址。
  2. 输入你的名字,按 Submit。
  3. 把 outputs="text" 改成 outputs="number",读报错。
  4. 给 greet 加第二个参数,看第二个框冒出来。

2 Blocks, for when you want to say where things go · Blocks สำหรับตอนที่คุณอยากบอกว่าอะไรอยู่ตรงไหน · Blocks,当你想指定东西摆哪

🇬🇧 English

Interface guesses the layout. Blocks lets you build it.

Everything made inside the with block belongs to the page, in the order you wrote it.

Nothing is wired until you call click.

🇹🇭 ไทย

Interface เดาเลย์เอาต์ให้ ส่วน Blocks ให้คุณสร้างเอง

ทุกอย่างที่สร้างใน with เป็นของหน้านั้น เรียงตามลำดับที่คุณเขียน

ยังไม่มีอะไรถูกต่อสายจนกว่าคุณจะเรียก click

🇨🇳 中文

Interface 替你猜布局。Blocks 让你自己搭。

with 里面做出来的每样东西都属于这个页面,按你写的顺序排。

在你调用 click 之前,什么线都没接。

📄 NEW FILE · blocks1.py

"""blocks1.py — Blocks, for when you want to say where things go."""
import gradio as gr


def count_words(sentence):
    """Count the words, and warn when counting words is the wrong idea."""
    words = sentence.split()
    counted = str(len(words)) + " words, " + str(len(sentence)) + " characters"

    # split() looks for spaces. Thai and Chinese do not put spaces between
    # words, so a whole sentence comes back as one item. Saying so is more
    # use to the reader than a number that is quietly wrong.
    if " " not in sentence.strip() and not sentence.isascii():
        return counted + "  <- no spaces, so split() found nothing to cut on"
    return counted


# Interface guesses the layout. Blocks lets you build it. Everything created
# inside the with block belongs to this page, in the order it is written.
with gr.Blocks(title="Count words") as demo:
    gr.Markdown("## Count the words\nType a sentence, in any language.")

    # Row puts its children side by side. Column stacks them.
    with gr.Row():
        sentence = gr.Textbox(label="Your sentence", scale=3)
        answer = gr.Textbox(label="What Python thinks", scale=2)

    with gr.Row():
        go = gr.Button("Count", variant="primary")
        clear = gr.Button("Clear")

    # Nothing is wired yet. click() says: run this function, take its
    # arguments from these components, put the answer in those.
    go.click(fn=count_words, inputs=sentence, outputs=answer)

    # A tiny function written where it is used. It takes nothing and returns
    # two empty strings, one for each output.
    clear.click(fn=lambda: ("", ""), inputs=None, outputs=[sentence, answer])

    gr.Examples(examples=["the cat sat on the mat", "ดวงวันนี้เป็นยังไง",
                          "今天的运势如何"],
                inputs=sentence)

if __name__ == "__main__":
    demo.launch()
LineWhat it does · ทำอะไร · 做什么
with gr.Blocks(title="Count words") as demo:Everything indented under this belongs to the page.ทุกอย่างที่ย่อหน้าอยู่ใต้บรรทัดนี้เป็นของหน้านี้缩在这行下面的所有东西都属于这个页面。
with gr.Row():Side by side. gr.Column() stacks them instead.วางเรียงข้างกัน ส่วน gr.Column() จะวางซ้อนลงมา并排。gr.Column() 则是叠着放。
go.click(fn=count_words, inputs=sentence, outputs=answer)Run this, take the arguments from there, put the answer here.รันอันนี้ เอาค่ามาจากตรงนั้น แล้วเอาคำตอบไปใส่ตรงนี้跑这个,参数从那儿拿,答案放这儿。
fn=lambda: ("", "")Two outputs, so return two things. A lambda is enough here.มีสองเอาต์พุต จึงคืนค่าสองอย่าง ตรงนี้ lambda ก็พอแล้ว两个输出,就返回两个东西。这里用 lambda 就够。
gr.Examples(examples=[Clickable examples. The fastest way to show what your page is for.ตัวอย่างที่คลิกได้ วิธีเร็วที่สุดในการบอกว่าหน้านี้มีไว้ทำอะไร可以点的示例。说明你这页是干嘛的最快办法。

🖥️ WHAT IT PRINTED · three sentences, three languages

the cat sat on the mat   -> 6 words, 22 characters
ดวงวันนี้เป็นยังไง       -> 1 words, 18 characters  <- no spaces, so split() found nothing to cut on
今天的运势如何            -> 1 words, 7 characters  <- no spaces, so split() found nothing to cut on
COUNTING WORDS IS A LANGUAGE PROBLEM, NOT A PYTHON ONE split() looks for spaces. Thai and Chinese do not put them between words. A whole sentence comes back as one item. Python sees nothing wrong. Say so on the page. A number that is quietly wrong is worse than no number. split() มองหาช่องว่าง แต่ภาษาไทยกับจีนไม่เว้นวรรคระหว่างคำ ประโยคทั้งประโยคจึงกลับมาเป็นชิ้นเดียว และในสายตา Python ก็ไม่มีอะไรผิด บอกเรื่องนี้ไว้บนหน้าเว็บด้วย ตัวเลขที่ผิดแบบเงียบ ๆ แย่กว่าการไม่มีตัวเลข split() 找的是空格,而泰文和中文不在词之间放空格。整句话会变成一项,而在 Python 看来毫无问题。把这件事写在页面上。一个悄悄错掉的数字,比没有数字更糟。
YOUR TURN
  1. Run it. Click each example in turn.
  2. Swap gr.Row() for gr.Column(). Look again.
  3. Change scale=3 and scale=2. What do they do?
  4. Add a third button that puts the sentence in backwards.
  1. รันดู แล้วคลิกตัวอย่างทีละอัน
  2. เปลี่ยน gr.Row() เป็น gr.Column() แล้วดูใหม่
  3. ลองเปลี่ยน scale=3 กับ scale=2 มันทำอะไร
  4. เพิ่มปุ่มที่สามที่ใส่ประโยคกลับหลัง
  1. 运行,把每个示例依次点一遍。
  2. 把 gr.Row() 换成 gr.Column(),再看一眼。
  3. 改 scale=3 和 scale=2。它们做什么?
  4. 再加第三个按钮,把句子倒过来放。

🤖 Tutorial 2 — The model in the browser · บทที่ 2 — โมเดลในเบราว์เซอร์ · 教程 2 —— 浏览器里的模型

🇬🇧 English

A whole chat page, transcript and history included, from one function.

Then the thing tkinter made hard: words appearing as they are written, from one keyword.

🇹🇭 ไทย

หน้าแชตทั้งหน้า พร้อมบทสนทนาและประวัติ จากฟังก์ชันเดียว

แล้วก็สิ่งที่ tkinter ทำได้ยาก คือตัวอักษรที่โผล่ขึ้นตอนกำลังเขียน ด้วยคำเดียว

🇨🇳 中文

一整个聊天页面,记录和历史都有,来自一个函数。

然后是 tkinter 做起来很麻烦的那件事:字边写边出现,只要一个关键字。

3 A chat page, and one trap in the history · หน้าแชต กับกับดักหนึ่งอันในประวัติ · 一个聊天页面,历史里有一个坑

🇬🇧 English

gr.ChatInterface builds the transcript, the box, the button and the history.

You write one function. It is called with what was typed, and everything said before.

The trap is in that history. Read the careful box before you trust it.

🇹🇭 ไทย

gr.ChatInterface สร้างบทสนทนา ช่องพิมพ์ ปุ่ม และประวัติให้ทั้งหมด

คุณเขียนฟังก์ชันเดียว มันจะถูกเรียกพร้อมสิ่งที่พิมพ์ และทุกอย่างที่พูดกันมาก่อนหน้า

กับดักอยู่ในประวัตินั้น อ่านกล่องคำเตือนก่อนจะเชื่อมัน

🇨🇳 中文

gr.ChatInterface 把记录、输入框、按钮和历史全建好。

你写一个函数。它会带着你打的字,和之前说过的一切,被调用。

坑就在那个历史里。在你信它之前,先读警告框。

📄 NEW FILE · chat1.py

"""chat1.py — the model in a browser, and it remembers what you said."""
import json
import urllib.request

import gradio as gr

OLLAMA = "http://localhost:11434/api/chat"
MODEL = "llama3.2"


def plain(content):
    """Turn one history entry's content into a plain string.

    Gradio hands back content as a list of parts, so that a turn can hold a
    picture as well as words. Ollama wants one string. This takes the words
    out and leaves everything else behind.
    """
    if isinstance(content, str):
        return content
    return "".join(part.get("text", "") for part in content
                   if part.get("type") == "text")


def reply(message, history):
    """Gradio calls this with what was typed, and everything said before."""
    # history is gradio's record of the conversation. Ollama wants the same
    # thing in its own shape, so copy it across one turn at a time.
    talk = [{"role": turn["role"], "content": plain(turn["content"])}
            for turn in history]
    talk.append({"role": "user", "content": message})

    payload = json.dumps({"model": MODEL, "stream": False,
                          "messages": talk}).encode()
    request = urllib.request.Request(
        OLLAMA, data=payload, headers={"Content-Type": "application/json"})
    with urllib.request.urlopen(request, timeout=300) as response:
        return json.load(response)["message"]["content"].strip()


# ChatInterface builds the whole page: the transcript, the box, the button,
# the history. You write one function and it does the rest.
demo = gr.ChatInterface(reply, title="Talk to a model on your own computer",
                        examples=["My name is Nari.", "What is my name?"])

if __name__ == "__main__":
    demo.launch()
LineWhat it does · ทำอะไร · 做什么
def reply(message, history):Two arguments, always in this order. Gradio fills them both.สองอาร์กิวเมนต์ เรียงแบบนี้เสมอ Gradio ใส่ให้ทั้งคู่两个参数,顺序永远是这个。Gradio 两个都替你填。
return "".join(part.get("text", "") for part in contentTake the words out of the parts, and leave the rest behind.ดึงเฉพาะคำออกมาจากส่วนต่าง ๆ แล้วทิ้งที่เหลือไว้把文字从各个部分里取出来,其余的留下不管。
talk = [{"role": turn["role"], "content": plain(turn["content"])}Gradio's record, copied into the shape Ollama wants.บันทึกของ Gradio ถูกคัดลอกให้เป็นรูปแบบที่ Ollama ต้องการ把 Gradio 的记录,抄成 Ollama 要的样子。
gr.ChatInterface(reply,The whole page, from that one function. Nothing else to build.ทั้งหน้าเว็บ จากฟังก์ชันเดียวนั้น ไม่ต้องสร้างอะไรอีก整个页面就从那一个函数来。没别的要建了。

🖥️ WHAT IT PRINTED · the history really was a list of parts

plain() on a list of parts -> 'My name is Nari.'
plain() on a plain string  -> 'already text'
reply with history         -> 'Your name is Nari.'
HISTORY CONTENT IS A LIST, NOT A STRING This caught me while writing the page. Gradio hands each turn over in this shape. {'role': 'user', 'content': [{'text': 'hi', 'type': 'text'}]}. The content is a list of parts. A turn can hold a picture too. Pass that straight to Ollama and it breaks. It wants a string. That is what plain() is for. เรื่องนี้เจอตอนเขียนหน้านี้เอง Gradio ส่งแต่ละเทิร์นมาแบบนี้ {'role': 'user', 'content': [{'text': 'hi', 'type': 'text'}]} ตัว content เป็นลิสต์ของส่วนต่าง ๆ เพื่อให้เทิร์นหนึ่งใส่รูปได้ด้วย ถ้าส่งแบบนั้นตรงไปให้ Ollama มันจะได้ลิสต์ทั้งที่ต้องการสตริง นั่นคือหน้าที่ของ plain() 这是写这一页时撞上的。Gradio 递过来的每一轮长这样:{'role': 'user', 'content': [{'text': 'hi', 'type': 'text'}]}。content 是一个部分组成的列表,好让一轮里也能放图片。直接把它丢给 Ollama,它会拿到一个列表,而它要的是字符串。plain() 就是干这个的。
YOUR TURN
  1. Run it. Say your name, then ask what it is.
  2. Print history at the top of reply. Read it.
  3. Delete plain( and use turn["content"]. See it break.
  4. Put it back, then give the model a system message.
  1. รันดู บอกชื่อคุณ แล้วถามว่าชื่ออะไร
  2. พิมพ์ history ออกมาที่ต้นฟังก์ชัน reply แล้วอ่านดู
  3. ลบ plain( แล้วใช้ turn["content"] ดูมันพัง
  4. ใส่กลับ แล้วลองให้ system message กับโมเดล
  1. 运行。说出你的名字,再问它你叫什么。
  2. 在 reply 开头打印 history,读一读。
  3. 删掉 plain(,改用 turn["content"],看它坏掉。
  4. 再改回来,然后给模型一条 system 消息。

4 Words appearing as they are written · ตัวอักษรที่โผล่ขึ้นตอนกำลังเขียน · 字边写边出现

🇬🇧 English

One keyword. yield instead of return.

The function hands back the answer many times, growing, and gradio redraws after each one.

In tkinter this took a thread and a queue. Here it is a keyword.

🇹🇭 ไทย

คำเดียว ใช้ yield แทน return

ฟังก์ชันจะคืนคำตอบกลับมาหลายครั้ง ยาวขึ้นเรื่อย ๆ และ gradio จะวาดใหม่ทุกครั้ง

ใน tkinter เรื่องนี้ต้องใช้เธรดกับคิว ที่นี่เป็นแค่คำเดียว

🇨🇳 中文

一个关键字。用 yield,不用 return。

函数把答案交回来很多次,一次比一次长,gradio 每次都重画。

在 tkinter 里这得靠线程加队列。在这儿就是一个关键字。

📄 NEW FILE · stream1.py

"""stream1.py — the words appear as the model writes them."""
import json
import urllib.request

import gradio as gr

from chat1 import plain

OLLAMA = "http://localhost:11434/api/chat"
MODEL = "llama3.2"


def reply(message, history):
    """Hand back the answer many times, growing, instead of once at the end.

    yield instead of return makes this a generator. Gradio reads it until it
    stops, and redraws the page after every value. The reader sees the
    sentence being written, which is the whole difference.
    """
    talk = [{"role": turn["role"], "content": plain(turn["content"])}
            for turn in history]
    talk.append({"role": "user", "content": message})

    # stream=True asks Ollama for the answer in pieces. It then sends one
    # line of JSON per piece, instead of one JSON object at the end.
    payload = json.dumps({"model": MODEL, "stream": True,
                          "messages": talk}).encode()
    request = urllib.request.Request(
        OLLAMA, data=payload, headers={"Content-Type": "application/json"})

    so_far = ""
    with urllib.request.urlopen(request, timeout=300) as response:
        for line in response:
            if not line.strip():
                continue
            piece = json.loads(line)
            so_far = so_far + piece["message"]["content"]

            # Send the whole answer so far, not just the new piece. Gradio
            # replaces what is on screen each time, it does not add to it.
            yield so_far


demo = gr.ChatInterface(reply, title="Watch it think")

if __name__ == "__main__":
    demo.launch()
LineWhat it does · ทำอะไร · 做什么
"stream": TrueOllama sends one line of JSON per piece. Not one at the end.Ollama จะส่ง JSON บรรทัดละชิ้น แทนที่จะส่งก้อนเดียวตอนจบOllama 每块发一行 JSON,而不是最后发一整个。
so_far = so_far + piece["message"]["content"]Each piece is a few letters. Glue them onto what you have.แต่ละชิ้นคือตัวอักษรไม่กี่ตัว เอามาต่อกับที่มีอยู่每块只是几个字母。粘到已有的后面。
yield so_farThe whole answer so far. Gradio replaces, it does not add.คำตอบทั้งหมดเท่าที่มี Gradio จะแทนที่ ไม่ใช่ต่อท้าย到目前为止的整个答案。Gradio 是替换,不是追加。
from chat1 import plainWritten once in step 3, used again here. That is why files import.เขียนไว้ครั้งเดียวในขั้นที่ 3 แล้วใช้ซ้ำตรงนี้ นี่คือเหตุผลที่ไฟล์อิมพอร์ตกัน第 3 步写过一次,这里再用。文件之间 import 就是为了这个。

🖥️ WHAT IT PRINTED · nine answers, each longer than the last

yielded 9 times, growing each time: True
first  -> '1'
last   -> '1, 2, 3.'
YIELD THE WHOLE ANSWER, NOT THE NEW PIECE Gradio replaces what is on screen with whatever you yield. Yield only the new letters and each one flickers past alone. Nothing is left behind. Keep a running string and yield that. Gradio จะแทนที่สิ่งที่อยู่บนจอด้วยค่าที่คุณ yield ออกมา ถ้า yield แค่ตัวอักษรใหม่ คนอ่านจะเห็นมันกะพริบผ่านไปทีละตัว โดยไม่มีอะไรเหลือค้างไว้ ให้เก็บสตริงสะสมไว้แล้ว yield อันนั้น Gradio 会用你 yield 的东西替换屏幕上的内容。只 yield 新的那几个字母,读的人就只看到它们一个个闪过去,什么都不留下。存一个累积的字符串,yield 那个。
YOUR TURN
  1. Run it. Ask for something long. Watch it arrive.
  2. Change yield so_far to yield the piece only. Look.
  3. Put it back.
  4. Add a time.sleep(0.05) in the loop. Slow it right down.
  1. รันดู ขออะไรยาว ๆ แล้วดูมันทยอยมา
  2. เปลี่ยน yield so_far ให้ yield แค่ชิ้นเดียว แล้วดู
  3. เปลี่ยนกลับ
  4. ใส่ time.sleep(0.05) ในลูป ให้มันช้าลงไปเลย
  1. 运行。要一段长的,看它一点点到。
  2. 把 yield so_far 改成只 yield 那一块,看看。
  3. 再改回来。
  4. 在循环里加 time.sleep(0.05),把它放得很慢。

🎛️ Tutorial 3 — Controls, and letting people in · บทที่ 3 — ตัวควบคุม และการให้คนอื่นเข้ามา · 教程 3 —— 控件,以及放人进来

🇬🇧 English

Text boxes are the least of it. Sliders hand you a number, and checkboxes hand you True or False.

Then the part to think about before you do it: who else can open this.

🇹🇭 ไทย

ช่องข้อความเป็นแค่ส่วนน้อย สไลเดอร์ส่งตัวเลขมาให้ และเช็กบ็อกซ์ส่ง True หรือ False มาให้

แล้วก็ส่วนที่ต้องคิดก่อนลงมือ ว่าใครอีกบ้างที่เปิดอันนี้ได้

🇨🇳 中文

文本框只是最小的一块。滑块直接给你数字,勾选框给你 True 或 False。

然后是动手前要先想的那部分:还有谁能打开它。

5 Every control becomes one argument · ทุกตัวควบคุมกลายเป็นหนึ่งอาร์กิวเมนต์ · 每个控件变成一个参数

🇬🇧 English

Five components in the list, five arguments in the function, in the same order.

A slider means you never have to check that a number is a number.

🇹🇭 ไทย

ห้าคอมโพเนนต์ในลิสต์ ห้าอาร์กิวเมนต์ในฟังก์ชัน เรียงลำดับเดียวกัน

สไลเดอร์แปลว่าคุณไม่ต้องคอยตรวจว่าตัวเลขเป็นตัวเลขจริงไหม

🇨🇳 中文

列表里五个组件,函数里五个参数,顺序一样。

有滑块,你就永远不用检查一个数字是不是数字。

📄 NEW FILE · inputs1.py

"""inputs1.py — sliders, dropdowns and switches, not only boxes."""
import json
import urllib.request

import gradio as gr

OLLAMA = "http://localhost:11434/api/chat"


def write_something(topic, model, temperature, sentences, in_thai):
    """Five arguments, five components, in this order.

    Interface hands the components over in the order they were listed, so
    the names here and the list below must line up.
    """
    order = "Write " + str(sentences) + " sentences about " + topic + "."
    if in_thai:
        order = order + " Answer in Thai."

    payload = json.dumps({
        "model": model,
        "stream": False,
        "options": {"temperature": temperature},
        "messages": [{"role": "user", "content": order}],
    }).encode()
    request = urllib.request.Request(
        OLLAMA, data=payload, headers={"Content-Type": "application/json"})
    with urllib.request.urlopen(request, timeout=300) as response:
        return json.load(response)["message"]["content"].strip()


demo = gr.Interface(
    fn=write_something,
    inputs=[
        gr.Textbox(label="About what?", value="the rain in Chiang Mai"),
        gr.Dropdown(["llama3.2", "qwen2.5:3b"], value="llama3.2",
                    label="Which model"),

        # A slider hands over a number, so you never have to check that what
        # the person typed is a number at all.
        gr.Slider(0, 1, value=0.8, step=0.1, label="Temperature"),
        gr.Slider(1, 6, value=3, step=1, label="How many sentences"),

        # A Checkbox hands over True or False.
        gr.Checkbox(label="Answer in Thai"),
    ],
    outputs=gr.Textbox(label="What it wrote", lines=6),
    title="Ask for something",
    description="Every control here becomes one argument to the function.",
)

if __name__ == "__main__":
    demo.launch()
LineWhat it does · ทำอะไร · 做什么
def write_something(topic, model, temperature, sentences, in_thai):Five names, matching the five components below, in order.ห้าชื่อ ตรงกับห้าคอมโพเนนต์ข้างล่าง เรียงตามลำดับ五个名字,对应下面五个组件,按顺序。
gr.Dropdown(["llama3.2", "qwen2.5:3b"], value="llama3.2",A closed list. Nobody can type a model you have not pulled.ลิสต์ปิด ไม่มีใครพิมพ์ชื่อโมเดลที่คุณยังไม่ได้โหลดมาได้一个封闭的列表。没人能打出你还没拉过的模型。
gr.Slider(0, 1, value=0.8, step=0.1, label="Temperature")Hands over a number between the two ends. Never a surprise.ส่งตัวเลขระหว่างสองปลายมาให้ ไม่มีอะไรเซอร์ไพรส์给你一个在两端之间的数字。永远不会有意外。
gr.Checkbox(label="Answer in Thai")Hands over True or False. No parsing, no guessing.ส่ง True หรือ False มาให้ ไม่ต้องแปลง ไม่ต้องเดา给你 True 或 False。不用解析,不用猜。

🖥️ WHAT IT PRINTED · two sentences about the rain

write_something('the rain in Chiang Mai', 'llama3.2', 0.8, 2, False)
-> 'Chiang Mai, located in northern Thailand, experiences a rainy season from May to October, with heavy rainfall...'
YOUR TURN
  1. Run it. Move the temperature to 0 and ask twice. Same answer?
  2. Move it to 1 and ask twice. Different?
  3. Tick the Thai box.
  4. Add a gr.Radio for the mood of the writing.
  1. รันดู เลื่อน temperature ไปที่ 0 แล้วถามสองครั้ง ได้คำตอบเดิมไหม
  2. เลื่อนไปที่ 1 แล้วถามสองครั้ง ต่างกันไหม
  3. ติ๊กช่องภาษาไทยดู
  4. เพิ่ม gr.Radio สำหรับอารมณ์ของงานเขียน
  1. 运行。把 temperature 拉到 0,问两次。答案一样吗?
  2. 拉到 1,再问两次。不一样了吗?
  3. 勾上泰文那个框。
  4. 加一个 gr.Radio,选写作的语气。

6 Letting somebody else open it · ให้คนอื่นเปิดมันได้ · 让别人也能打开它

🇬🇧 English

By default the page answers this computer only. There are three ways further out, and each is a bigger step.

🇹🇭 ไทย

โดยค่าเริ่มต้น หน้านี้ตอบเฉพาะเครื่องนี้ มีสามทางที่ไกลออกไป และแต่ละทางก็ก้าวใหญ่ขึ้นเรื่อย ๆ

🇨🇳 中文

默认这个页面只回应这台电脑。往外还有三条路,一条比一条迈得大。

What you writeคุณเขียนอะไร你写什么Who can open itใครเปิดได้谁能打开Think aboutต้องคิดถึง要想清楚
demo.launch()this computer onlyเฉพาะเครื่องนี้只有这台电脑nothingไม่มี没什么
server_name="0.0.0.0"anyone on the same wifiใครก็ได้ที่อยู่ไวไฟเดียวกัน同一个 wifi 上的人a cafe wifi is not your wifiไวไฟร้านกาแฟไม่ใช่ไวไฟของคุณ咖啡店的 wifi 不是你的 wifi
auth=(user, password)people you gave the passwordคนที่คุณให้รหัสผ่านไป你给了密码的人one password for everybodyรหัสผ่านเดียวสำหรับทุกคน所有人共用一个密码
share=Trueanyone on the internetใครก็ได้บนอินเทอร์เน็ต互联网上的任何人read the warning belowอ่านคำเตือนข้างล่าง读下面的警告

📄 NEW FILE · share1.py

"""share1.py — letting somebody else open it, carefully."""
import os

import gradio as gr

# Read the password from the environment, the same as the email pages do.
# A password typed into a file ends up in git, and git does not forget.
USER = os.environ.get("APP_USER", "student")
PASSWORD = os.environ.get("APP_PASS")


def shout(words):
    return words.upper() + "!"


demo = gr.Interface(fn=shout, inputs="text", outputs="text",
                    title="Only for people with the password")

if __name__ == "__main__":
    # auth puts a login page in front of everything. Without a password set,
    # refuse to start rather than open a door and hope nobody walks in.
    if not PASSWORD:
        raise SystemExit("Set APP_PASS first. No password, no server.")

    # server_name="0.0.0.0" answers other machines on the same wifi.
    # Leave it out and only this computer can reach the page.
    #
    # share=True is the other way: gradio opens a tunnel and prints a
    # public address anyone on the internet can open. Read the warning on
    # this page before you use it.
    demo.launch(auth=(USER, PASSWORD), server_name="0.0.0.0",
                server_port=7860)
LineWhat it does · ทำอะไร · 做什么
PASSWORD = os.environ.get("APP_PASS")From the environment. A password in a file ends up in git.มาจาก environment รหัสผ่านที่อยู่ในไฟล์จะไปจบที่ git从环境变量来。写在文件里的密码,最后会进 git。
raise SystemExit("Set APP_PASS first. No password, no server.")Refuse to start. Do not open a door and hope.ปฏิเสธที่จะเริ่ม อย่าเปิดประตูทิ้งไว้แล้วหวังว่าจะไม่มีใครเข้า干脆不启动。别开着门然后祈祷。
demo.launch(auth=(USER, PASSWORD), server_name="0.0.0.0",A login page in front, and answers other machines on the wifi.มีหน้าล็อกอินกั้นอยู่หน้าสุด และตอบเครื่องอื่นในไวไฟด้วย前面挡一个登录页,同时也回应 wifi 上的其他机器。

🖥️ WHAT IT PRINTED · with and without the password

no APP_PASS set  -> exit code 1, the server never started
with auth set    -> GET /config  401 Unauthorized
SHARE=TRUE PUTS YOUR COMPUTER ON THE INTERNET Gradio opens a tunnel and prints a public address. Anyone with that address reaches the program on your machine. Your function runs for them. Use auth with it. Never leave it running. Never point it at your own files. The link in this step was never created. That is not a thing to do accidentally. Gradio จะเปิดอุโมงค์แล้วพิมพ์ที่อยู่สาธารณะออกมา ใครก็ตามที่มีที่อยู่นั้นจะเข้าถึงโปรแกรมบนเครื่องของคุณได้ และฟังก์ชันของคุณจะทำงานให้เขา ใช้ auth ควบคู่เสมอ อย่าเปิดทิ้งไว้ และอย่าชี้มันไปที่โฟลเดอร์ไฟล์ส่วนตัวของคุณ ลิงก์ในขั้นนี้ไม่เคยถูกสร้างขึ้นเลย เพราะการสร้างมันไม่ใช่เรื่องที่ควรทำโดยบังเอิญ Gradio 会开一条隧道,并打印一个公开地址。任何拿到这个地址的人都能连到你机器上的程序,你的函数会替他们跑。一定要配上 auth,别让它一直开着,也别把它指向你自己的文件夹。这一步里的链接从来没有被创建过,因为创建它不该是个意外。
YOUR TURN
  1. Run it with no APP_PASS. It should refuse.
  2. Set one and run it again. A login page appears.
  3. Open it from your phone on the same wifi.
  4. Stop it. Do not leave it running overnight.
  1. รันโดยไม่ตั้ง APP_PASS มันควรปฏิเสธ
  2. ตั้งค่าแล้วรันใหม่ จะมีหน้าล็อกอินโผล่มา
  3. ลองเปิดจากมือถือที่อยู่ไวไฟเดียวกัน
  4. แล้วหยุดมัน อย่าเปิดทิ้งไว้ข้ามคืน
  1. 不设 APP_PASS 就运行。它应该拒绝。
  2. 设一个再运行。会出现登录页。
  3. 用同一个 wifi 上的手机打开看看。
  4. 然后停掉。别让它开一整夜。

🏆 Tutorial 4 — Five challenges · บทที่ 4 — โจทย์ห้าข้อ · 教程 4 —— 五道题

🇬🇧 English

Each one is a page you would actually keep. Try it before you open the answer.

🇹🇭 ไทย

แต่ละข้อคือหน้าเว็บที่คุณจะเก็บไว้ใช้จริง ลองทำเองก่อนเปิดเฉลย

🇨🇳 中文

每一道都是你真的会留着用的页面。先自己试,再打开答案。

7 Try first. The answers are folded away. · ลองเองก่อน เฉลยพับซ่อนไว้ · 先自己试。答案折起来放着。

🇬🇧 English

Every answer here was run before it was written down.

🇹🇭 ไทย

ทุกเฉลยในนี้ถูกรันจริงก่อนจะถูกเขียนลงมา

🇨🇳 中文

这里每个答案都是先跑过,才写下来的。

1The coin price, with two answers at once · ราคาเหรียญ พร้อมคำตอบสองอย่าง · 币价,一次给两个答案

🇬🇧 English

A dropdown of coins and a button. Show the price and how far it moved today.

Two outputs, so the function returns two things. Fill it in when the page opens, not only on a click.

🇹🇭 ไทย

ดรอปดาวน์ของเหรียญกับปุ่มหนึ่งปุ่ม แสดงราคาและระยะที่มันขยับวันนี้

มีสองเอาต์พุต ฟังก์ชันจึงคืนค่าสองอย่าง ให้เติมค่าตอนเปิดหน้าด้วย ไม่ใช่แค่ตอนคลิก

🇨🇳 中文

一个币种下拉框和一个按钮。显示价格,以及今天涨跌了多少。

两个输出,所以函数返回两个东西。页面一打开就填上,别只在点击时才填。

Show the answerดูเฉลย看答案

📄 price_app.py

"""price_app.py — challenge 2: the coin price, in a browser."""
import json
import urllib.request

import gradio as gr

BITKUB = "https://api.bitkub.com/api/v3/market/ticker"


def price_of(coin):
    """Ask Bitkub what one coin is worth in baht."""
    pair = coin + "_THB"
    with urllib.request.urlopen(BITKUB + "?sym=" + pair, timeout=30) as response:
        coins = json.load(response)
    if not coins:
        return "Bitkub does not know " + coin, ""

    # Bitkub sends a list even for one coin, and every number arrives as text.
    # The field is percent_change, with an underscore. Print the whole answer
    # once and read the names; guessing them costs more time than looking.
    now = coins[0]
    price = float(now["last"])
    moved = float(now["percent_change"])

    arrow = "up" if moved >= 0 else "down"
    return (format(price, ",.2f") + " baht",
            arrow + " " + format(abs(moved), ".2f") + "% today")


with gr.Blocks(title="Coin prices") as demo:
    gr.Markdown("## What is it worth right now?")
    with gr.Row():
        coin = gr.Dropdown(["BTC", "ETH", "XRP", "DOGE"], value="BTC",
                           label="Coin")
        go = gr.Button("Check", variant="primary")
    with gr.Row():
        baht = gr.Textbox(label="One coin costs")
        change = gr.Textbox(label="Since midnight")

    # Two outputs, so the function returns two things, in this order.
    go.click(fn=price_of, inputs=coin, outputs=[baht, change])

    # Run it once as the page opens, so it is never blank.
    demo.load(fn=price_of, inputs=coin, outputs=[baht, change])

if __name__ == "__main__":
    demo.launch()
LineWhat it does · ทำอะไร · 做什么
moved = float(now["percent_change"])percent_change, with an underscore. I guessed wrong first.percent_change มีขีดล่าง ตอนแรกผมเดาผิดpercent_change,带下划线。我第一次猜错了。
outputs=[baht, change])Two outputs in a list, so return two things in that order.สองเอาต์พุตในลิสต์ จึงคืนค่าสองอย่างตามลำดับนั้น列表里两个输出,就按那个顺序返回两个东西。
demo.load(fn=price_of,Runs as the page opens, so it is never blank.ทำงานตอนหน้าเว็บเปิด หน้าจะได้ไม่ว่างเปล่า页面一打开就跑,所以它从不空着。

🖥️ WHAT IT PRINTED · live, from Bitkub

BTC -> ('2,799,289.64 baht', 'down 2.13% today')
ETH -> ('89,288.30 baht', 'down 1.88% today')
READ THE FIELD NAMES, DO NOT GUESS THEM I wrote percentChange from memory and got a KeyError. The real name is percent_change. Print the whole answer once and read it. Guessing a field name costs more than looking it up. ผมเขียน percentChange จากความจำแล้วได้ KeyError ชื่อจริงคือ percent_change ให้พิมพ์คำตอบทั้งก้อนออกมาดูสักครั้งแล้วอ่าน การเดาชื่อฟิลด์เสียเวลากว่าการเปิดดู 我凭记忆写了 percentChange,换来一个 KeyError。真名是 percent_change。把整个响应打印出来读一遍。猜字段名比查一下更费时间。

2What Python really thinks your text is · Python คิดว่าข้อความของคุณคืออะไรกันแน่ · Python 到底怎么看你的文字

🇬🇧 English

Paste anything and answer three questions. How many words. What the letters really are. Is it a number.

Three outputs. Try it with Thai, Chinese, and Thai numerals.

🇹🇭 ไทย

วางอะไรก็ได้ แล้วตอบสามคำถาม มีกี่คำ ตัวอักษรจริง ๆ คืออะไร และมันเป็นตัวเลขไหม

สามเอาต์พุต ลองกับภาษาไทย จีน และเลขไทย

🇨🇳 中文

粘贴任何东西,回答三个问题:几个词、那些字母到底是什么、它是不是数字。

三个输出。用泰文、中文和泰文数字各试一次。

Show the answerดูเฉลย看答案

📄 words_app.py

"""words_app.py — challenge 3: what Python really thinks your text is."""
import unicodedata

import gradio as gr


def look_closely(text):
    """Three questions about one piece of text, and three honest answers."""
    if not text:
        return "", "", ""

    # split() looks for spaces. Thai and Chinese do not use them between
    # words, so a whole sentence comes back as a single item.
    words = text.split()
    counted = (str(len(words)) + " by split(), "
               + str(len(text)) + " characters")
    if len(words) == 1 and len(text) > 12:
        counted = counted + "  <- split() found no spaces to cut on"

    # len() counts code points. A Thai vowel or tone mark is its own one,
    # so the number is often larger than the letters a reader sees.
    pieces = []
    for letter in text[:14]:
        pieces.append(letter + " = " + unicodedata.name(letter, "unknown"))
    inside = "\n".join(pieces)

    # isdigit is true for Thai numerals, and int() reads them.
    numbers = ""
    if text.strip().isdigit():
        numbers = text.strip() + " is a number to Python: " + str(int(text.strip()))
    elif text.strip().isnumeric():
        numbers = (text.strip() + " is numeric but not a digit. Its value is "
                   + str(unicodedata.numeric(text.strip()[0])))
    else:
        numbers = "not a number"

    return counted, inside, numbers


demo = gr.Interface(
    fn=look_closely,
    inputs=gr.Textbox(label="Paste anything", lines=3),
    outputs=[gr.Textbox(label="How many words"),
             gr.Textbox(label="What the first letters really are", lines=8),
             gr.Textbox(label="Is it a number?")],
    title="What Python thinks your text is",
    examples=[["the cat sat on the mat"], ["ดวงวันนี้เป็นยังไง"],
              ["今天的运势如何"], ["๑๒๓"], ["café"]],
)

if __name__ == "__main__":
    demo.launch()
LineWhat it does · ทำอะไร · 做什么
unicodedata.name(letter, "unknown")The official name of one character. Thai vowels are separate ones.ชื่อทางการของอักขระหนึ่งตัว สระไทยเป็นอักขระแยกต่างหาก一个字符的正式名字。泰文的元音是独立的字符。
if text.strip().isdigit():True for Thai numerals as well, and int() reads them.เป็นจริงกับเลขไทยด้วย และ int() อ่านมันได้泰文数字也为真,而且 int() 读得懂它们。
elif text.strip().isnumeric():Chinese numbers are numeric but not digits. Different questions.เลขจีนเป็น numeric แต่ไม่ใช่ digit คนละคำถามกัน中文数字是 numeric 但不是 digit。两个不同的问题。

🖥️ WHAT IT PRINTED · four things pasted in

the cat sat    3 by split(), 11 characters      not a number
ดวงวันนี้      1 by split(), 9 characters       not a number
๑๒๓            1 by split(), 3 characters       is a number to Python: 123
café           1 by split(), 4 characters       not a number

3A picture in, a different picture out · รูปเข้าไป รูปที่ต่างออกมา · 进去一张图,出来另一张

🇬🇧 English

Drop a picture in, pick what to do to it, and get it back changed.

type="pil" hands your function a Pillow image instead of a file path.

🇹🇭 ไทย

ลากรูปใส่เข้าไป เลือกว่าจะทำอะไรกับมัน แล้วรับรูปที่เปลี่ยนแล้วกลับมา

type="pil" จะส่งรูปแบบ Pillow ให้ฟังก์ชันของคุณ แทนที่จะเป็นพาธของไฟล์

🇨🇳 中文

把图片丢进来,选要对它做什么,再拿回改过的那张。

type="pil" 递给你函数的是一个 Pillow 图像,不是文件路径。

Show the answerดูเฉลย看答案

📄 image_app.py

"""image_app.py — challenge 4: a picture goes in, a changed picture comes out."""
import gradio as gr
from PIL import Image, ImageFilter, ImageOps


def change(picture, what, angle):
    """Take a picture and hand a different one back.

    type="pil" on the input means gradio gives this function a Pillow
    image, not a file path and not a grid of numbers.
    """
    if picture is None:
        return None, ""

    if what == "grey":
        changed = ImageOps.grayscale(picture)
    elif what == "blur":
        changed = picture.filter(ImageFilter.GaussianBlur(4))
    elif what == "edges":
        changed = picture.convert("L").filter(ImageFilter.FIND_EDGES)
    else:
        changed = ImageOps.invert(picture.convert("RGB"))

    if angle:
        # expand=True grows the canvas so a turned corner is not cut off.
        changed = changed.rotate(angle, expand=True, resample=Image.BICUBIC)

    return changed, (what + ", turned " + str(angle) + " degrees, now "
                     + str(changed.width) + "x" + str(changed.height))


with gr.Blocks(title="Change a picture") as demo:
    gr.Markdown("## Drop a picture in")
    with gr.Row():
        with gr.Column():
            before = gr.Image(type="pil", label="Yours")
            what = gr.Radio(["grey", "blur", "edges", "invert"],
                            value="grey", label="Do what to it")
            angle = gr.Slider(0, 270, value=0, step=90, label="Turn it")
            go = gr.Button("Change it", variant="primary")
        with gr.Column():
            after = gr.Image(label="Changed")
            note = gr.Textbox(label="What happened")

    go.click(fn=change, inputs=[before, what, angle], outputs=[after, note])

if __name__ == "__main__":
    demo.launch()
LineWhat it does · ทำอะไร · 做什么
before = gr.Image(type="pil", label="Yours")pil gives a Pillow image. Leave it out and you get numbers.pil ให้รูปแบบ Pillow ถ้าไม่ใส่ คุณจะได้ตัวเลขมาแทนpil 给的是 Pillow 图像。不写的话你拿到的是一堆数字。
if picture is None:Somebody will press the button with nothing dropped in.จะมีคนกดปุ่มโดยที่ยังไม่ได้ใส่อะไรลงไป总会有人什么都没放就按按钮。
changed.rotate(angle, expand=True,expand grows the canvas, so a turned corner is not cut off.expand ขยายผืนผ้าใบ มุมที่หมุนไปจะได้ไม่ถูกตัดexpand 会把画布撑大,转过去的角才不会被切掉。

🖥️ WHAT IT PRINTED · one real PNG, four ways

opened ac_title.png at 898x674
grey    -> grey, turned 90 degrees, now 674x898
blur    -> blur, turned 90 degrees, now 674x898
edges   -> edges, turned 90 degrees, now 674x898
invert  -> invert, turned 90 degrees, now 674x898

4A quiz that keeps your score, and only yours · ควิซที่เก็บคะแนนของคุณ และของคุณคนเดียว · 一个记分的小测验,只记你的

🇬🇧 English

Ask a Thai word, offer four answers, keep the score.

Here is the catch. Two people open the page at once. A plain variable would be shared between them.

Find the thing gradio gives you instead.

🇹🇭 ไทย

ถามคำภาษาไทย ให้เลือกสี่ตัวเลือก แล้วเก็บคะแนน

แต่มีข้อติดอยู่ สองคนเปิดหน้านี้พร้อมกัน ตัวแปรธรรมดาจะถูกใช้ร่วมกันระหว่างเขาสองคน

ไปหาสิ่งที่ gradio ให้มาใช้แทน

🇨🇳 中文

问一个泰文词,给四个选项,记住分数。

但有个坑:两个人同时打开这一页。一个普通变量会被他们俩共用。

去找 gradio 给你的那个替代品。

Show the answerดูเฉลย看答案

📄 quiz_app.py

"""quiz_app.py — challenge 4: a quiz that keeps your score, and only yours."""
import random

import gradio as gr

WORDS = [("cat", "แมว"), ("rain", "ฝน"), ("market", "ตลาด"),
         ("mountain", "ภูเขา"), ("rice", "ข้าว"), ("teacher", "ครู"),
         ("friend", "เพื่อน"), ("water", "น้ำ")]


def ask_one():
    """Pick a word and the three wrong answers to show beside it."""
    english, thai = random.choice(WORDS)
    wrong = random.sample([t for _, t in WORDS if t != thai], 3)
    choices = wrong + [thai]
    random.shuffle(choices)
    return english, thai, choices


def start():
    """Set up the first question and an empty score."""
    english, thai, choices = ask_one()
    score = {"right": 0, "asked": 0, "answer": thai}
    return ("How do you say **" + english + "**?",
            gr.Radio(choices=choices, value=None, label="Pick one"),
            "", score)


def answer(chosen, score):
    """Mark it, then ask the next one.

    score arrives as this browser's own copy and is handed straight back.
    A plain global would be shared by everyone who opened the page.
    """
    if chosen is None:
        return gr.skip(), gr.skip(), "Pick one first.", score

    score["asked"] = score["asked"] + 1
    if chosen == score["answer"]:
        score["right"] = score["right"] + 1
        said = "Right. " + chosen
    else:
        said = "No, it was " + score["answer"]

    english, thai, choices = ask_one()
    score["answer"] = thai
    running = (said + "   —   " + str(score["right"]) + " of "
               + str(score["asked"]))
    return ("How do you say **" + english + "**?",
            gr.Radio(choices=choices, value=None, label="Pick one"),
            running, score)


with gr.Blocks(title="Thai words") as demo:
    gr.Markdown("## Thai words")
    question = gr.Markdown()
    choices = gr.Radio(choices=[], label="Pick one")
    result = gr.Markdown()

    # State is invisible. Every browser that opens this page gets its own,
    # and it is handed to any function that asks for it.
    score = gr.State({"right": 0, "asked": 0, "answer": ""})

    go = gr.Button("Answer", variant="primary")
    go.click(fn=answer, inputs=[choices, score],
             outputs=[question, choices, result, score])

    demo.load(fn=start, inputs=None,
              outputs=[question, choices, result, score])

if __name__ == "__main__":
    demo.launch()
LineWhat it does · ทำอะไร · 做什么
score = gr.State({"right": 0, "asked": 0, "answer": ""})Invisible, and one copy per browser that opens the page.มองไม่เห็น และมีหนึ่งชุดต่อหนึ่งเบราว์เซอร์ที่เปิดหน้านี้看不见,每个打开页面的浏览器各有一份。
def answer(chosen, score):State comes in as an argument and goes back as a result.State เข้ามาเป็นอาร์กิวเมนต์ และกลับออกไปเป็นผลลัพธ์State 作为参数进来,再作为结果出去。
return gr.skip(), gr.skip(),skip means leave that component exactly as it is.skip แปลว่าปล่อยคอมโพเนนต์นั้นไว้อย่างเดิมเป๊ะ ๆskip 的意思是那个组件原样不动。
gr.Radio(choices=choices, value=None,Returning a component replaces its options, not only its value.การคืนค่าเป็นคอมโพเนนต์จะแทนที่ตัวเลือกของมัน ไม่ใช่แค่ค่า返回一个组件会换掉它的选项,不只是它的值。

🖥️ WHAT IT PRINTED · right, wrong, and nothing picked

start  -> How do you say **market**?   answer is ตลาด
right  -> Right. ตลาด   -   1 of 1
wrong  -> No, it was ครู   -   1 of 2
none   -> Pick one first.
score kept: {'right': 1, 'asked': 2}
A GLOBAL WOULD BE EVERYBODY'S SCORE This page is on a web server. Two people can open it at the same second. A module level variable is one variable, shared by all of them. The second person would inherit the first person's score. gr.State gives each browser its own. หน้านี้อยู่บนเว็บเซิร์ฟเวอร์ สองคนเปิดพร้อมกันในวินาทีเดียวได้ ตัวแปรระดับโมดูลมีแค่ตัวเดียว ใช้ร่วมกันทุกคน คนที่สองจะรับคะแนนของคนแรกมาเลย gr.State ให้แต่ละเบราว์เซอร์มีของตัวเอง 这一页跑在 web 服务器上。两个人可能在同一秒打开它。模块级别的变量只有一个,所有人共用,第二个人会直接继承第一个人的分数。gr.State 让每个浏览器各有一份。

5Give the model a tool, and stand back · ให้เครื่องมือกับโมเดล แล้วถอยออกมา · 给模型一个工具,然后退后

🇬🇧 English

Every button so far was wired by you. You decided when it ran.

Write the price lookup as a @tool, hand it to the model, and ask in English.

An agent takes tens of seconds, so say something first.

🇹🇭 ไทย

ทุกปุ่มที่ผ่านมาคุณเป็นคนต่อสายเอง คุณเป็นคนตัดสินใจว่ามันทำงานเมื่อไหร่

เขียนการดูราคาเป็น @tool ยื่นให้โมเดล แล้วถามเป็นภาษาอังกฤษ

เอเจนต์ใช้เวลาหลายสิบวินาที ให้พูดอะไรสักอย่างก่อน

🇨🇳 中文

到目前为止每个按钮都是你接的线,什么时候跑你说了算。

把查价格写成一个 @tool,递给模型,用英文问。

一个 agent 要花几十秒,所以先说点什么。

💻 INSTALL THESE TOO

pip install smolagents openai
Show the answerดูเฉลย看答案

📄 agent_app.py

"""agent_app.py — challenge 5: give the model a tool, in a browser."""
import json
import os
import urllib.request

import gradio as gr
from smolagents import OpenAIServerModel, ToolCallingAgent, tool

# Where the model runs. These defaults use Ollama on this computer.
LLM_URL = os.environ.get("LLM_URL", "http://localhost:11434/v1")
# A small model that is good at tool calls beats a big one that is not.
# A 14B on a laptop with no graphics card takes about a minute per answer.
LLM_MODEL = os.environ.get("LLM_MODEL", "llama3.2")

# The OpenAI client always sends a key. Ollama accepts any word.
LLM_KEY = os.environ.get("LLM_KEY", "ollama")

BITKUB = "https://api.bitkub.com/api/v3/market/ticker"


@tool
def coin_price(symbol: str) -> str:
    """Look up what one coin is worth in Thai baht, right now.

    Args:
        symbol: the coin's ticker, for example BTC or ETH
    """
    # No double quotes in the line above. That description is copied into
    # the tool list the model is shown, and a small model will copy it
    # straight back into its answer, quotes and all, which is then not
    # valid JSON. It costs a whole step every time.
    pair = symbol.upper().strip() + "_THB"
    with urllib.request.urlopen(BITKUB + "?sym=" + pair, timeout=30) as response:
        coins = json.load(response)
    if not coins:
        return "no coin called " + symbol + " on Bitkub"
    return ("1 " + symbol.upper() + " = "
            + format(float(coins[0]["last"]), ",.2f") + " baht")


llm = OpenAIServerModel(model_id=LLM_MODEL, api_base=LLM_URL, api_key=LLM_KEY)

# The list of tools is the whole permission system. A tool that is not in
# this list cannot be called, however nicely anyone asks.
agent = ToolCallingAgent(tools=[coin_price], model=llm)


def think(message, history):
    """Say something while it works, then replace it with the answer.

    An agent takes tens of seconds. yield puts a word on the screen at once,
    so the page does not just sit there looking broken.
    """
    yield "thinking, and deciding whether to look anything up..."
    try:
        # max_steps so a model that cannot work out what to do gives up,
        # rather than looping until you close the tab.
        yield str(agent.run(message, max_steps=6))
    except Exception as trouble:
        yield "no answer: " + str(trouble)


demo = gr.ChatInterface(
    think,
    title="Ask about coins",
    description="Nobody wired a button. The model decides when to look.",
    examples=["What is one bitcoin worth in baht?",
              "Is ETH cheaper than BTC today?",
              "What is the capital of Thailand?"])

if __name__ == "__main__":
    demo.launch()
LineWhat it does · ทำอะไร · 做什么
@toolTurns a plain function into something a model may call.เปลี่ยนฟังก์ชันธรรมดาให้เป็นสิ่งที่โมเดลเรียกใช้ได้把一个普通函数变成模型可以调用的东西。
def coin_price(symbol: str) -> str:The types are required. The model reads them to call it right.ต้องระบุชนิดข้อมูล โมเดลอ่านมันเพื่อจะเรียกได้ถูก类型是必须的。模型靠读它们才知道怎么调。
ToolCallingAgent(tools=[coin_price], model=llm)This list is the whole permission system. Not in it, not callable.ลิสต์นี้คือระบบสิทธิ์ทั้งหมด ไม่อยู่ในนี้ก็เรียกไม่ได้这个列表就是全部的权限系统。不在里面就调不了。
yield "thinking, and deciding whether to look anything up..."Something on screen at once, so the page does not look broken.ให้มีอะไรขึ้นจอทันที หน้าเว็บจะได้ไม่ดูเหมือนพัง立刻在屏幕上放点东西,页面才不像坏了。
symbol: the coin's ticker, for example BTC or ETHNo double quotes. The careful box below says what they break.ห้ามมีอัญประกาศคู่ กล่องคำเตือนข้างล่างบอกว่ามันพังอะไร不要用双引号。下面的警告框说明它们会破坏什么。
agent.run(message, max_steps=6)A model that cannot work it out gives up, instead of looping.โมเดลที่คิดไม่ออกจะยอมแพ้ แทนที่จะวนไปเรื่อย ๆ想不明白的模型会放弃,而不是一直绕圈。

🖥️ WHAT IT PRINTED · llama3.2 on this laptop, 6.5 seconds

default model: llama3.2
Calling tool: 'coin_price' with arguments: {'symbol': 'BTC'}
Observations: 1 BTC = 2,815,197.21 baht
Calling tool: 'final_answer'
Final answer: 2,815,197.21
6.5s, yielded 2

🇬🇧 English

You never wrote BTC anywhere. The model took it out of the question.

Asked the capital of Thailand, it tried to call web_search, which it does not have. Then it answered Bangkok from memory.

The tools list is what stopped it.

🇹🇭 ไทย

คุณไม่เคยเขียน BTC ไว้ที่ไหนเลย โมเดลดึงมันออกมาจากคำถามเอง

พอถามว่าเมืองหลวงของไทยคืออะไร มันพยายามเรียก web_search ซึ่งมันไม่มี แล้วก็ตอบว่ากรุงเทพจากความจำ

ลิสต์เครื่องมือคือสิ่งที่หยุดมันไว้

🇨🇳 中文

你从没在任何地方写过 BTC,是模型自己从问题里挑出来的。

问它泰国的首都时,它试着去调 web_search,而它并没有这个工具。然后它凭记忆答了曼谷。

拦住它的,就是那个工具列表。

NO DOUBLE QUOTES IN A TOOL DOCSTRING This tool first put quotation marks around BTC and ETH. smolagents shows that description to the model. A small model copies it back into its answer, quotes and all. The JSON no longer parses. A reader hit it on step 1. Write examples without quotation marks. เครื่องมือนี้ตอนแรกอธิบายอาร์กิวเมนต์ของมันว่า the coin's ticker, like "BTC" or "ETH" smolagents จะแสดงคำอธิบายนั้นให้โมเดลเห็น โมเดลเล็กก็คัดลอกมันกลับมาในคำตอบ พร้อมอัญประกาศครบ แล้ว JSON ก็แปลงค่าไม่ได้อีกต่อไป มีคนอ่านเจอตั้งแต่ขั้นที่ 1 ให้เขียนตัวอย่างโดยไม่ใส่เครื่องหมายคำพูด 这个工具一开始把它的参数描述成 the coin's ticker, like "BTC" or "ETH"。smolagents 会把那段描述给模型看。小模型连引号一起抄回自己的答案里,JSON 就解析不了了。有读者在第 1 步就撞上了。写例子时别用引号。
A MODEL RETYPING A NUMBER IS A NUMBER YOU DID NOT CHECK In one run the agent relayed the price exactly. In another it turned 2,797,568.60 into 2,797,000.00. The tool was right both times. If the figure matters, show the tool's own answer. Not the model's retelling. ในการรันครั้งหนึ่ง เอเจนต์ส่งต่อราคามาตรงเป๊ะ อีกครั้งหนึ่งมันเปลี่ยน 2,797,568.60 เป็น 2,797,000.00 เครื่องมือถูกต้องทั้งสองครั้ง ถ้าตัวเลขนั้นสำคัญ ให้แสดงคำตอบของเครื่องมือเอง ไม่ใช่คำเล่าของโมเดล 有一次运行里,agent 把价格原样转达了。另一次它把 2,797,568.60 变成了 2,797,000.00。两次工具都是对的。如果这个数字重要,就显示工具自己的答案,而不是模型转述的版本。

🇬🇧 English

Eleven files, and not one line of HTML. That is the whole pitch.

Gradio is for showing a function to somebody. It is not for building a website, and it does not try to be.

When the page needs to look like yours, that is the moment to reach for Astro instead.

🇹🇭 ไทย

สิบเอ็ดไฟล์ และไม่มี HTML สักบรรทัด นั่นคือจุดขายทั้งหมด

Gradio มีไว้โชว์ฟังก์ชันให้คนอื่นดู ไม่ได้มีไว้สร้างเว็บไซต์ และมันก็ไม่ได้พยายามจะเป็นแบบนั้น

เมื่อไหร่ที่หน้าเว็บต้องหน้าตาเป็นของคุณเอง นั่นคือจังหวะที่ควรหันไปใช้ Astro แทน

🇨🇳 中文

十一个文件,没有一行 HTML。卖点全在这。

Gradio 是拿来把一个函数给别人看的。它不是用来做网站的,也没打算是。

当页面需要长成你自己的样子时,那就是该转去用 Astro 的时候。

You wantคุณอยากได้你想要Look upไปหาคำว่า去查
Tabs across the topแท็บด้านบน顶上的标签页gr.Tabs
A box that folds awayกล่องที่พับเก็บได้可以折起来的框gr.Accordion
A table of rowsตารางหลายแถว一张有行的表gr.Dataframe
A chartกราฟ图表gr.Plot
Sound in or outเสียงเข้าหรือออก声音进或出gr.Audio
Put it on the internetเอาขึ้นอินเทอร์เน็ต放到互联网上Hugging Face SpacesHugging Face SpacesHugging Face Spaces

Fifteen lines, your own address, and an app password. That is the whole trick. · สิบห้าบรรทัด อีเมลของคุณเอง และ app password หนึ่งอัน เคล็ดลับมีแค่นี้ · 十五行代码、你自己的邮箱地址,加一个应用专用密码。诀窍就这些。

All eleven files on this page were run on 24 September 2026 against gradio 6.28.0, and every output shown is copied from those runs. Each app was served for real and fetched over HTTP to check it answered. The history format in step 3 was found by launching a chat page, typing into it in a browser, and writing down what the function was handed - it is a list of parts, not a string, and that is a change from gradio 5. The Bitkub field name in challenge 1 is there because I guessed percentChange from memory and got a KeyError; the real name is percent_change. Challenge 5 first shipped with qwen2.5:14b and no step limit. A reader found the same tool on the classes page appearing to freeze, twice: once because a 14B on a laptop with no graphics card takes about a minute, and once because the tool’s own docstring said like "BTC" or "ETH" and the model copied those quotation marks into its JSON. It now defaults to llama3.2 and caps the agent at six steps; the 6.5 second run shown is the fixed file. No share=True link was ever created, so the only claim on this page that was not run is the one warning you about it.