Gradio Agents & MCP 黑客马拉松

获奖者
Gradio logo
  1. 使用 Blocks 构建
  2. 使用 Render 装饰器的动态应用

使用 Render 装饰器的动态应用

到目前为止,你在 Blocks 中定义的组件和事件监听器都是固定的——一旦演示启动,就无法添加新组件和监听器,也无法删除现有组件和监听器。

@gr.render 装饰器引入了动态更改此功能的能力。让我们一起来看看。

动态组件数量

在下面的示例中,我们将创建可变数量的文本框。当用户编辑输入文本框时,我们会为输入中的每个字母创建一个文本框。请在下面尝试一下

import gradio as gr

with gr.Blocks() as demo:
    input_text = gr.Textbox(label="input")

    @gr.render(inputs=input_text)
    def show_split(text):
        if len(text) == 0:
            gr.Markdown("## No Input Provided")
        else:
            for letter in text:
                gr.Textbox(letter)

demo.launch()

看看我们现在如何使用自定义逻辑(在本例中是一个简单的 for 循环)创建可变数量的文本框。@gr.render 装饰器通过以下步骤实现此功能

  1. 创建一个函数并为其附加 @gr.render 装饰器。
  2. 将输入组件添加到 @gr.render 的 inputs= 参数,并在函数中为每个组件创建一个相应的参数。此函数将在组件发生任何更改时自动重新运行。
  3. 将所有要根据输入渲染的组件添加到函数内部。

现在,每当输入发生变化时,函数就会重新运行,并用最新运行创建的组件替换之前运行创建的组件。相当简单!让我们为这个应用增加一点复杂性

import gradio as gr

with gr.Blocks() as demo:
    input_text = gr.Textbox(label="input")
    mode = gr.Radio(["textbox", "button"], value="textbox")

    @gr.render(inputs=[input_text, mode], triggers=[input_text.submit])
    def show_split(text, mode):
        if len(text) == 0:
            gr.Markdown("## No Input Provided")
        else:
            for letter in text:
                if mode == "textbox":
                    gr.Textbox(letter)
                else:
                    gr.Button(letter)

demo.launch()

默认情况下,@gr.render 的重新运行是由应用的 .load 监听器和所提供的任何输入组件的 .change 监听器触发的。我们可以通过在装饰器中显式设置触发器来覆盖此行为,就像我们在这个应用中只在 input_text.submit 上触发一样。如果你正在设置自定义触发器,并且还希望在应用启动时自动渲染,请确保将 demo.load 添加到你的触发器列表中。

动态事件监听器

如果你正在创建组件,你可能也想为其附加事件监听器。让我们看一个示例,该示例接受可变数量的文本框作为输入,并将所有文本合并到一个框中。

import gradio as gr

with gr.Blocks() as demo:
    text_count = gr.State(1)
    add_btn = gr.Button("Add Box")
    add_btn.click(lambda x: x + 1, text_count, text_count)

    @gr.render(inputs=text_count)
    def render_count(count):
        boxes = []
        for i in range(count):
            box = gr.Textbox(key=i, label=f"Box {i}")
            boxes.append(box)

        def merge(*args):
            return " ".join(args)

        merge_btn.click(merge, boxes, output)

    merge_btn = gr.Button("Merge")
    output = gr.Textbox(label="Merged Output")

demo.launch()

让我们看看这里发生了什么

  1. 状态变量 text_count 跟踪要创建的文本框数量。通过单击“添加”按钮,我们增加 text_count,这会触发 render 装饰器。
  2. 请注意,在我们在 render 函数中创建的每个文本框中,我们都显式设置了一个 key= 参数。此键允许我们在重新渲染之间保留此组件的值。如果你在文本框中输入一个值,然后单击“添加”按钮,所有文本框都会重新渲染,但它们的值不会被清除,因为 key= 在渲染过程中维护了组件的值。
  3. 我们将创建的文本框存储在一个列表中,并将此列表作为输入提供给合并按钮事件监听器。请注意,所有使用在 render 函数内部创建的组件的事件监听器也必须在该 render 函数内部定义。事件监听器仍然可以引用 render 函数外部的组件,就像我们在这里通过引用在 render 函数外部定义的 merge_btnoutput 所做的那样。

与组件一样,每当函数重新渲染时,上一次渲染创建的事件监听器会被清除,并附加最新运行的新事件监听器。

这使我们能够创建高度可定制和复杂的交互!

深入了解 keys= 参数

key= 参数用于让 Gradio 知道当你的 render 函数重新运行时,正在生成相同的组件。这会产生两个效果

  1. 浏览器中相同的元素会从上一次渲染中为该组件重新使用。这带来了浏览器性能提升——因为在渲染时无需销毁和重建组件——并保留了组件可能具有的任何浏览器属性。如果你的组件嵌套在 gr.Row 等布局项中,请确保它们也已加键,因为父项的键也必须匹配。
  2. 用户或其他事件监听器可能更改的属性会得到保留。默认情况下,只保留组件的“值”,但你可以使用 preserved_by_key= kwarg 指定要保留的任何属性列表。

请看下面的示例

import gradio as gr
import random

with gr.Blocks() as demo:
    number_of_boxes = gr.Slider(1, 5, step=1, value=3, label="Number of Boxes")

    @gr.render(inputs=[number_of_boxes])
    def create_boxes(number_of_boxes):
        for i in range(number_of_boxes):
            with gr.Row(key=f'row-{i}'):
                number_box = gr.Textbox(
                    label=f"Default Label", 
                    info="Default Info", 
                    key=f"box-{i}", 
                    preserved_by_key=["label", "value"], 
                    interactive=True
                )
                change_label_btn = gr.Button("Change Label", key=f"btn-{i}")

                change_label_btn.click(
                    lambda: gr.Textbox(
                        label=random.choice("ABCDE"), 
                        info=random.choice("ABCDE")), 
                        outputs=number_box
                )

demo.launch()

在此示例中,当你更改 number_of_boxes 滑块时,会触发新的重新渲染以更新文本框行的数量。如果你单击“更改标签”按钮,它们会更改相应文本框的 labelinfo 属性。你也可以在任何文本框中输入文本来更改其值。如果在此之后更改文本框数量,重新渲染会“重置” info,但 label 和任何输入的值仍将保留。

请注意,你也可以为任何事件监听器设置键,例如 button.click(key=...),如果相同的监听器在多次渲染中以相同的输入和输出重新创建。这会带来性能优势,并且还可以在事件在上次渲染中触发,然后发生重新渲染,然后上次事件完成处理时防止错误发生。通过为监听器设置键,Gradio 知道如何正确发送数据。

整合起来

让我们看两个使用了上述所有功能的示例。首先,请尝试下面的待办事项列表应用

import gradio as gr

with gr.Blocks() as demo:

    tasks = gr.State([])
    new_task = gr.Textbox(label="Task Name", autofocus=True)

    def add_task(tasks, new_task_name):
        return tasks + [{"name": new_task_name, "complete": False}], ""

    new_task.submit(add_task, [tasks, new_task], [tasks, new_task])

    @gr.render(inputs=tasks)
    def render_todos(task_list):
        complete = [task for task in task_list if task["complete"]]
        incomplete = [task for task in task_list if not task["complete"]]
        gr.Markdown(f"### Incomplete Tasks ({len(incomplete)})")
        for task in incomplete:
            with gr.Row():
                gr.Textbox(task['name'], show_label=False, container=False)
                done_btn = gr.Button("Done", scale=0)
                def mark_done(task=task):
                    task["complete"] = True
                    return task_list
                done_btn.click(mark_done, None, [tasks])

                delete_btn = gr.Button("Delete", scale=0, variant="stop")
                def delete(task=task):
                    task_list.remove(task)
                    return task_list
                delete_btn.click(delete, None, [tasks])

        gr.Markdown(f"### Complete Tasks ({len(complete)})")
        for task in complete:
            gr.Textbox(task['name'], show_label=False, container=False)

demo.launch()

请注意,几乎整个应用程序都在一个响应任务 gr.State 变量的 gr.render 中。此变量是一个嵌套列表,这会带来一些复杂性。如果你设计 gr.render 来响应列表或字典结构,请确保执行以下操作

  1. 任何以应触发重新渲染的方式修改状态变量的事件监听器都必须将该状态变量设置为输出。这让 Gradio 知道在幕后检查变量是否已更改。
  2. gr.render 中,如果循环中的变量在事件监听函数内部使用,则应通过将其自身设置为函数头中的默认参数来“冻结”该变量。请注意我们在 mark_donedelete 中都有 task=task。这会将变量冻结到其“循环时”的值。

让我们看最后一个使用了所有我们所学知识的示例。下面是一个音频混音器。提供多个音轨并将它们混合在一起。

import gradio as gr

with gr.Blocks() as demo:
    track_count = gr.State(1)
    add_track_btn = gr.Button("Add Track")

    add_track_btn.click(lambda count: count + 1, track_count, track_count)

    @gr.render(inputs=track_count)
    def render_tracks(count):
        audios = []
        volumes = []
        with gr.Row():
            for i in range(count):
                with gr.Column(variant="panel", min_width=200):
                    gr.Textbox(placeholder="Track Name", key=f"name-{i}", show_label=False)
                    track_audio = gr.Audio(label=f"Track {i}", key=f"track-{i}")
                    track_volume = gr.Slider(0, 100, value=100, label="Volume", key=f"volume-{i}")
                    audios.append(track_audio)
                    volumes.append(track_volume)

            def merge(data):
                sr, output = None, None
                for audio, volume in zip(audios, volumes):
                    sr, audio_val = data[audio]
                    volume_val = data[volume]
                    final_track = audio_val * (volume_val / 100)
                    if output is None:
                        output = final_track
                    else:
                        min_shape = tuple(min(s1, s2) for s1, s2 in zip(output.shape, final_track.shape))
                        trimmed_output = output[:min_shape[0], ...][:, :min_shape[1], ...] if output.ndim > 1 else output[:min_shape[0]]
                        trimmed_final = final_track[:min_shape[0], ...][:, :min_shape[1], ...] if final_track.ndim > 1 else final_track[:min_shape[0]]
                        output += trimmed_output + trimmed_final
                return (sr, output)

            merge_btn.click(merge, set(audios + volumes), output_audio)

    merge_btn = gr.Button("Merge Tracks")
    output_audio = gr.Audio(label="Output", interactive=False)

demo.launch()

此应用中有两点需要注意

  1. 在这里,我们为所有组件提供了 key=!我们需要这样做,以便在为现有音轨设置值之后添加另一个音轨时,现有音轨的输入值不会在重新渲染时被重置。
  2. 当有许多不同类型和任意数量的组件传递给事件监听器时,使用集合和字典表示法作为输入比列表表示法更容易。在上面,当我们向 merge 函数传递输入时,我们创建了一个包含所有输入 gr.Audiogr.Slider 组件的大型集合。在函数体中,我们将组件值作为字典查询。

gr.render 极大地扩展了 Gradio 的功能——看看你能用它做什么吧!