Gradio Agents & MCP Hackathon

获奖者
Gradio logo
  1. 使用 Blocks 构建
  2. 块和事件监听器

块和事件监听器

我们在快速入门中简要介绍了 Blocks 类,作为构建自定义演示的一种方式。现在让我们深入了解。

Blocks 结构

请看下面的演示。

import gradio as gr


def greet(name):
    return "Hello " + name + "!"


with gr.Blocks() as demo:
    name = gr.Textbox(label="Name")
    output = gr.Textbox(label="Output Box")
    greet_btn = gr.Button("Greet")
    greet_btn.click(fn=greet, inputs=name, outputs=output, api_name="greet")

demo.launch()

  • 首先,请注意 with gr.Blocks() as demo: 语句。Blocks 应用代码将包含在此语句中。
  • 接下来是组件。这些组件与 Interface 中使用的组件相同。然而,它们不是作为参数传递给某个构造函数,而是在 with 语句中创建时自动添加到 Blocks 中。
  • 最后是 click() 事件监听器。事件监听器定义了应用内的数据流。在上面的示例中,监听器将两个文本框连接起来。文本框 name 作为 greet 方法的输入,文本框 output 作为其输出。当按钮 greet_btn 被点击时,此数据流将被触发。像 Interface 一样,事件监听器可以有多个输入或输出。

您也可以使用装饰器附加事件监听器——跳过 fn 参数,直接分配 inputsoutputs

import gradio as gr

with gr.Blocks() as demo:
    name = gr.Textbox(label="Name")
    output = gr.Textbox(label="Output Box")
    greet_btn = gr.Button("Greet")

    @greet_btn.click(inputs=name, outputs=output)
    def greet(name):
        return "Hello " + name + "!"

demo.launch()

事件监听器和交互性

在上面的示例中,您会注意到文本框 name 可以编辑,但文本框 output 不能。这是因为任何作为事件监听器输入的组件都被设置为可交互。然而,由于文本框 output 仅作为输出,Gradio 认定它不应被设置为可交互。您可以使用布尔型 interactive 关键字参数(例如 gr.Textbox(interactive=True))来覆盖默认行为,直接配置组件的交互性。

output = gr.Textbox(label="Output", interactive=True)

注意:如果 Gradio 组件既不是输入也不是输出会发生什么?如果组件使用默认值构建,则假定它正在显示内容并呈现为不可交互。否则,它将呈现为可交互。同样,此行为可以通过为 interactive 参数指定值来覆盖。

事件监听器类型

请看下面的演示

import gradio as gr

def welcome(name):
    return f"Welcome to Gradio, {name}!"

with gr.Blocks() as demo:
    gr.Markdown(
    """
    # Hello World!
    Start typing below to see the output.
    """)
    inp = gr.Textbox(placeholder="What is your name?")
    out = gr.Textbox()
    inp.change(welcome, inp, out)

demo.launch()

welcome 函数不是由点击触发,而是由在文本框 inp 中输入触发。这是由于 change() 事件监听器。不同的组件支持不同的事件监听器。例如,Video 组件支持 play() 事件监听器,当用户按下播放时触发。有关每个组件的事件监听器,请参阅文档

多数据流

Blocks 应用不像 Interface 那样局限于单个数据流。请看下面的演示。

import gradio as gr

def increase(num):
    return num + 1

with gr.Blocks() as demo:
    a = gr.Number(label="a")
    b = gr.Number(label="b")
    atob = gr.Button("a > b")
    btoa = gr.Button("b > a")
    atob.click(increase, a, b)
    btoa.click(increase, b, a)

demo.launch()

请注意,num1 可以作为 num2 的输入,反之亦然!随着您的应用变得越来越复杂,您将会有许多数据流连接各种组件。

这是一个“多步骤”演示的示例,其中一个模型(语音转文本模型)的输出被馈送到下一个模型(情感分类器)。

from transformers import pipeline

import gradio as gr

asr = pipeline("automatic-speech-recognition", "facebook/wav2vec2-base-960h")
classifier = pipeline("text-classification")

def speech_to_text(speech):
    text = asr(speech)["text"]  
    return text

def text_to_sentiment(text):
    return classifier(text)[0]["label"]  

demo = gr.Blocks()

with demo:
    audio_file = gr.Audio(type="filepath")
    text = gr.Textbox()
    label = gr.Label()

    b1 = gr.Button("Recognize Speech")
    b2 = gr.Button("Classify Sentiment")

    b1.click(speech_to_text, inputs=audio_file, outputs=text)
    b2.click(text_to_sentiment, inputs=text, outputs=label)

demo.launch()

函数输入:列表 vs 字典

到目前为止您看到的事件监听器都只有一个输入组件。如果您希望有多个输入组件向函数传递数据,您有两种选择来让函数接受输入组件的值:

  1. 作为参数列表,或
  2. 作为单个值字典,以组件为键

让我们看一个例子

import gradio as gr

with gr.Blocks() as demo:
    a = gr.Number(label="a")
    b = gr.Number(label="b")
    with gr.Row():
        add_btn = gr.Button("Add")
        sub_btn = gr.Button("Subtract")
    c = gr.Number(label="sum")

    def add(num1, num2):
        return num1 + num2
    add_btn.click(add, inputs=[a, b], outputs=c)

    def sub(data):
        return data[a] - data[b]
    sub_btn.click(sub, inputs={a, b}, outputs=c)

demo.launch()

add()sub() 都将 ab 作为输入。但是,这些监听器之间的语法不同。

  1. 对于 add_btn 监听器,我们将输入作为列表传递。函数 add() 将这些输入中的每一个作为参数。a 的值映射到参数 num1b 的值映射到参数 num2
  2. 对于 sub_btn 监听器,我们将输入作为集合传递(注意花括号!)。函数 sub() 接受一个单独的字典参数 data,其中键是输入组件,值是这些组件的值。

选择哪种语法取决于您的偏好!对于有许多输入组件的函数,选项 2 可能更容易管理。

函数返回:列表 vs 字典

同样,您可以将多个输出组件的值作为以下形式返回:

  1. 值列表,或
  2. 以组件为键的字典

让我们先看一个(1)的例子,其中我们通过返回两个值来设置两个输出组件的值:

with gr.Blocks() as demo:
    food_box = gr.Number(value=10, label="Food Count")
    status_box = gr.Textbox()

    def eat(food):
        if food > 0:
            return food - 1, "full"
        else:
            return 0, "hungry"

    gr.Button("Eat").click(
        fn=eat,
        inputs=food_box,
        outputs=[food_box, status_box]
    )

在上方代码中,每个返回语句都返回两个值,分别对应 food_boxstatus_box

注意:如果您的事件监听器只有一个输出组件,则**不应**将其作为单项列表返回。这将无法工作,因为 Gradio 不知道是否应将该外部列表解释为您返回值的一部分。您应该直接返回该值。

现在,让我们看看选项(2)。您不再需要按照输出组件的顺序返回一个值列表,而是可以返回一个字典,其中键对应输出组件,值是新值。这还允许您跳过更新某些输出组件。

with gr.Blocks() as demo:
    food_box = gr.Number(value=10, label="Food Count")
    status_box = gr.Textbox()

    def eat(food):
        if food > 0:
            return {food_box: food - 1, status_box: "full"}
        else:
            return {status_box: "hungry"}

    gr.Button("Eat").click(
        fn=eat,
        inputs=food_box,
        outputs=[food_box, status_box]
    )

注意当没有食物时,我们只更新了 status_box 元素。我们跳过了更新 food_box 组件。

当事件监听器返回时影响许多组件,或者有条件地影响部分输出而非其他输出时,字典返回非常有用。

请记住,在使用字典返回时,我们仍然需要指定事件监听器中可能的输出。

更新组件配置

事件监听器函数的返回值通常是对应输出组件的更新值。有时我们也想更新组件的配置,例如可见性。在这种情况下,我们返回一个新的组件,设置我们想要更改的属性。

import gradio as gr

def change_textbox(choice):
    if choice == "short":
        return gr.Textbox(lines=2, visible=True)
    elif choice == "long":
        return gr.Textbox(lines=8, visible=True, value="Lorem ipsum dolor sit amet")
    else:
        return gr.Textbox(visible=False)

with gr.Blocks() as demo:
    radio = gr.Radio(
        ["short", "long", "none"], label="What kind of essay would you like to write?"
    )
    text = gr.Textbox(lines=2, interactive=True, show_copy_button=True)
    radio.change(fn=change_textbox, inputs=radio, outputs=text)

demo.launch()

看看我们如何通过新的 gr.Textbox() 方法来配置 Textbox 本身。value= 参数仍然可以用于在组件配置的同时更新值。任何我们未设置的参数都将保留其先前的值。

不更改组件的值

在某些情况下,您可能希望保持组件的值不变。Gradio 包含一个特殊函数 gr.skip(),可以从您的函数中返回。返回此函数将保持输出组件(或多个组件)的值不变。让我们用一个例子来说明:

import random
import gradio as gr

with gr.Blocks() as demo:
    with gr.Row():
        clear_button = gr.Button("Clear")
        skip_button = gr.Button("Skip")
        random_button = gr.Button("Random")
    numbers = [gr.Number(), gr.Number()]

    clear_button.click(lambda : (None, None), outputs=numbers)
    skip_button.click(lambda : [gr.skip(), gr.skip()], outputs=numbers)
    random_button.click(lambda : (random.randint(0, 100), random.randint(0, 100)), outputs=numbers)

demo.launch()

请注意返回 None(通常会将组件值重置为空状态)与返回 gr.skip()(保持组件值不变)之间的区别。

提示: 如果您有多个输出组件,并且希望它们的值都保持不变,您可以直接返回单个 gr.skip(),而不是返回一个包含每个元素的跳过元组。

连续运行事件

您还可以通过使用事件监听器的 then 方法来连续运行事件。这将在前一个事件运行结束后运行当前事件。这对于分多步更新组件的事件非常有用。

例如,在下面的聊天机器人示例中,我们首先立即用用户消息更新聊天机器人,然后在模拟延迟后用计算机响应更新聊天机器人。

import gradio as gr
import random
import time

with gr.Blocks() as demo:
    chatbot = gr.Chatbot()
    msg = gr.Textbox()
    clear = gr.Button("Clear")

    def user(user_message, history):
        return "", history + [[user_message, None]]

    def bot(history):
        bot_message = random.choice(["How are you?", "I love you", "I'm very hungry"])
        time.sleep(2)
        history[-1][1] = bot_message
        return history

    msg.submit(user, [msg, chatbot], [msg, chatbot], queue=False).then(
        bot, chatbot, chatbot
    )
    clear.click(lambda: None, None, chatbot, queue=False)

demo.launch()

事件监听器的 .then() 方法会执行后续事件,无论前一个事件是否引发了任何错误。如果您希望只有在前一个事件成功执行后才运行后续事件,请使用 .success() 方法,它的参数与 .then() 相同。

将多个触发器绑定到同一个函数

很多时候,您可能希望将多个触发器绑定到同一个函数。例如,您可能希望允许用户点击提交按钮或按下回车键来提交表单。您可以使用 gr.on 方法并向 trigger 传递触发器列表来实现此目的。

import gradio as gr

with gr.Blocks() as demo:
    name = gr.Textbox(label="Name")
    output = gr.Textbox(label="Output Box")
    greet_btn = gr.Button("Greet")
    trigger = gr.Textbox(label="Trigger Box")

    def greet(name, evt_data: gr.EventData):
        return "Hello " + name + "!", evt_data.target.__class__.__name__

    def clear_name(evt_data: gr.EventData):
        return ""

    gr.on(
        triggers=[name.submit, greet_btn.click],
        fn=greet,
        inputs=name,
        outputs=[output, trigger],
    ).then(clear_name, outputs=[name])

demo.launch()

您也可以使用装饰器语法

import gradio as gr

with gr.Blocks() as demo:
    name = gr.Textbox(label="Name")
    output = gr.Textbox(label="Output Box")
    greet_btn = gr.Button("Greet")

    @gr.on(triggers=[name.submit, greet_btn.click], inputs=name, outputs=output)
    def greet(name):
        return "Hello " + name + "!"

demo.launch()

您可以使用 gr.on 通过绑定实现 change 事件的组件的 change 事件来创建“实时”事件。如果您不指定任何触发器,该函数将自动绑定到所有包含 change 事件的输入组件的 change 事件(例如 gr.Textboxchange 事件,而 gr.Button 没有)。

import gradio as gr

with gr.Blocks() as demo:
    with gr.Row():
        num1 = gr.Slider(1, 10)
        num2 = gr.Slider(1, 10)
        num3 = gr.Slider(1, 10)
    output = gr.Number(label="Sum")

    @gr.on(inputs=[num1, num2, num3], outputs=output)
    def sum(a, b, c):
        return a + b + c

demo.launch()

您可以像任何常规事件监听器一样在 gr.on 后面加上 .then。这个方便的方法应该能帮您节省大量重复代码!

将组件值直接绑定到其他组件的函数

如果您想将组件的值始终设置为其他组件值的函数,您可以使用以下简写方式:

with gr.Blocks() as demo:
  num1 = gr.Number()
  num2 = gr.Number()
  product = gr.Number(lambda a, b: a * b, inputs=[num1, num2])

这在功能上与以下代码相同:

with gr.Blocks() as demo:
  num1 = gr.Number()
  num2 = gr.Number()
  product = gr.Number()

  gr.on(
    [num1.change, num2.change, demo.load], 
    lambda a, b: a * b, 
    inputs=[num1, num2], 
    outputs=product
  )