到目前为止,你在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装饰器通过以下步骤实现这一点:
inputs=参数中,并在您的函数中为每个组件创建相应的参数。此函数将在任何组件更改时自动重新运行。现在每当输入发生变化时,函数会重新运行,并用最新运行的结果替换之前函数运行创建的组件。非常简单明了!让我们为这个应用增加一些复杂性:
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 添加到您的触发器列表中。
如果你正在创建组件,你可能也想为它们附加事件监听器。让我们看一个例子,该例子接受可变数量的Textbox作为输入,并将所有文本合并到一个单独的框中。
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()
让我们看看这里发生了什么:
text_count 用于跟踪要创建的文本框的数量。通过点击添加按钮,我们增加 text_count,这会触发渲染装饰器。key=参数。这个键允许我们在重新渲染之间保留该组件的值。如果你在一个文本框中输入一个值,然后点击添加按钮,所有的文本框都会重新渲染,但它们的值不会被清除,因为key=在渲染过程中保持了组件的值。merge_btn和output一样,它们都是在渲染函数外部定义的。与组件一样,每当函数重新渲染时,从前一次渲染创建的事件监听器会被清除,并附加最新运行的新事件监听器。
这使我们能够创建高度可定制和复杂的交互!
让我们来看两个使用上述所有功能的例子。首先,尝试下面的待办事项列表应用程序:
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.render中,该函数对任务gr.State变量做出反应。这个变量是一个嵌套列表,这带来了一些复杂性。如果你设计一个gr.render来对列表或字典结构做出反应,请确保执行以下操作:
gr.render中,如果在事件监听器函数中使用了循环中的变量,应该通过在函数头中将其设置为默认参数来“冻结”该变量。请参阅我们在mark_done和delete中如何设置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()
在这个应用中需要注意的两点:
key=!我们需要这样做,以便在为现有轨道设置值后添加另一个轨道时,现有轨道的输入值不会在重新渲染时被重置。gr.Audio和gr.Slider组件组成一个大集合,当我们将输入传递给merge函数时。在函数体中,我们将组件值作为字典进行查询。gr.render 极大地扩展了 gradio 的功能 - 看看你能用它做出什么!