State in Blocks 块中的状态
我们介绍了接口中的状态,这个指南将看看块中的状态,其工作原理大致相同。
全局状态
块中的全局状态与接口中的工作原理相同。在函数调用外创建的任何变量都是所有用户共享的引用。
会话状态
Gradio 支持会话状态,在块应用程序中,数据在页面会话中多次提交后仍然持续存在。重申一下,会话数据不会在模型的不同用户之间共享。要在会话状态中存储数据,您需要做三件事:
创建一个
gr.State()
对象。如果这个有状态的对象有默认值,请将其传递给构造函数。在事件监听器中,将
State
对象作为输入和输出。在事件监听器函数中,将变量添加到输入参数和返回值中。
让我们来看一场挂人游戏。
import gradio as gr # 导入gradio库secret_word = "gradio" # 设置游戏的秘密单词with gr.Blocks() as demo: # 使用gr.Blocks创建一个应用布局 used_letters_var = gr.State([]) # 使用状态变量记录已使用过的字母with gr.Row() as row: # 创建一行用于并排放置输入框和显示框with gr.Column(): # 创建第一列,用于放置输入文本框和猜测按钮input_letter = gr.Textbox(label="Enter letter") # 创建输入文本框,用于输入字母btn = gr.Button("Guess Letter") # 创建按钮,用于提交猜测的字母with gr.Column(): # 创建第二列,用于放置挂人游戏进度和已使用字母显示框hangman = gr.Textbox(label="Hangman",value="_"*len(secret_word) # 初始化挂人游戏的显示,用下划线代表秘密单词的每个字母)used_letters_box = gr.Textbox(label="Used Letters") # 创建一个文本框,用于显示已猜过的字母def guess_letter(letter, used_letters): # 定义猜测字母的函数used_letters.append(letter) # 将猜测的字母添加到已使用字母列表answer = "".join([(letter if letter in used_letters else "_") # 根据已猜测的字母更新挂人游戏的显示for letter in secret_word])return {used_letters_var: used_letters, # 更新状态变量中的已使用字母列表used_letters_box: ", ".join(used_letters), # 更新已使用字母文本框的内容hangman: answer # 更新挂人游戏显示框的内容}btn.click(guess_letter, # 将按钮点击事件绑定到guess_letter函数[input_letter, used_letters_var], # 指定输入参数:输入的字母和已使用字母列表[used_letters_var, used_letters_box, hangman] # 指定更新的目标组件:状态、已使用字母显示框和挂人游戏显示框)
demo.launch() # 启动应用
这段代码使用Gradio库创建了一个Web应用程序,实现了一个简单的挂人(Hangman)游戏。游戏的目标是猜测一个秘密单词,这里秘密单词被设置为"gradio"。
用户通过输入框输入一个字母,点击"Guess Letter"按钮提交猜测。
游戏记录用户已经猜测过的字母,并更新挂人进度显示以及显示已用字母列表。如果猜测的字母在秘密单词中,那么对应位置的下划线会被替换为实际字母;否则,猜测的字母只会出现在已用字母列表中。
guess_letter
函数处理字母猜测过程,包括更新游戏进度和已用字母。每次猜测后,游戏状态通过Gradio的State
组件进行更新,以保留游戏进度。GUI布局采用了行和列的组织方式,以清晰地分隔输入、游戏进度显示和已用字母列表。
这个示例展示了Gradio如何用于实现交互式Web应用,以及如何利用状态管理维护应用的动态数据。
让我们看看我们如何在这个游戏中执行上面列出的 3 个步骤:
我们在
used_letters_var
中存储使用过的字母。在State
的构造函数中,我们将这个的初始值设置为[]
,一个空列表。在
btn.click()
中,我们在输入和输出中都有对used_letters_var
的引用。在
guess_letter
中,我们将这个State
的值传递给used_letters
,然后在返回语句中返回这个State
的更新值。
对于更复杂的应用程序,您可能会在单个 Blocks 应用程序中有许多 State 变量存储会话状态。
了解更多关于 State
的信息,请查阅文档。https://gradio.app/docs/state
Dynamic Apps with the Render Decorator
动态应用程序与渲染装饰器
在 Blocks 中定义的组件和事件监听器到目前为止都是固定的 - 一旦演示启动,就不能添加新的组件和监听器,也不能移除已有的。
@gr.render
装饰器引入了动态更改这一能力的可能。让我们来看一下。
动态组件数量
在下面的示例中,我们将创建一个可变数量的文本框。当用户编辑输入文本框时,我们会为输入中的每个字母创建一个文本框。请在下面尝试:
import gradio as gr # 导入gradio库with gr.Blocks() as demo: # 使用gr.Blocks创建一个应用布局input_text = gr.Textbox(label="input") # 创建一个输入文本框,用于输入字符串@gr.render(inputs=input_text) # 使用gr.render修饰器绑定输入和渲染逻辑def show_split(text): # 定义处理和展示分割文本的函数if len(text) == 0: # 检查输入文本长度,如果为空则显示提示信息return gr.Markdown("## No Input Provided") # 使用Markdown组件显示没有输入的提示信息else:components = [] # 初始化一个组件列表,用于存放每个字符对应的Textbox组件for letter in text: # 遍历输入文本的每个字符components.append(gr.Textbox(value=letter)) # 为每个字符创建一个Textbox组件,并将其添加到列表中return components # 返回包含所有字符Textbox组件的列表demo.launch() # 启动应用
需要注意的是,原始代码中的 show_split
函数在条件分支中直接使用了 gr.Markdown
和 gr.Textbox
创建新组件的方式,但在 gr.render
修饰的函数中,正确的做法是返回渲染组件对象(例如,gr.Markdown
或者一组 gr.Textbox
组件)而不是直接在函数中实例化这些组件。
此代码的功能是:用户在Web应用中输入一个字符串,应用将根据这个字符串的每个字符创建一个单独的 Textbox
组件展示出来。如果用户没有输入任何文本,应用将显示一个Markdown组件提醒用户“没有提供输入”。
这个示例展示了Gradio中如何利用装饰器来处理用户输入,并根据输入动态生成和渲染组件,从而增加应用的交互性和动态性。
看看我们现在如何使用自定义逻辑创建可变数量的文本框 - 在这种情况下,一个简单的 for
循环。 @gr.render
装饰器通过以下步骤实现这一点:
创建一个函数并将@gr.render 装饰器附加到它上。
将输入组件添加到@gr.render 的
inputs=
参数中,并为函数中的每个组件创建一个相应的参数。任何对组件的更改都会自动重新运行此函数。根据输入添加函数内部想要渲染的所有组件。
现在,每当输入改变时,函数就会重新运行,并用最新运行的结果替换之前函数运行创建的组件。非常直接!让我们给这个应用程序增加一点更复杂的内容:
import gradio as gr # 导入gradio库with gr.Blocks() as demo: # 使用gr.Blocks创建一个应用布局input_text = gr.Textbox(label="input") # 创建一个输入文本框,用于输入字符串mode = gr.Radio(["textbox", "button"], value="textbox") # 创建单选按钮,让用户选择输出模式(文本框或按钮)@gr.render(inputs=[input_text, mode], triggers=[input_text.submit]) # 使用gr.render修饰器绑定输入和触发逻辑def show_split(text, mode): # 定义处理和展示分割文本的函数if len(text) == 0: # 检查输入文本长度,如果为空则显示提示信息return gr.Markdown("## No Input Provided") # 使用Markdown组件显示没有输入的提示信息else:components = [] # 初始化一个组件列表,用于存放根据字符和模式生成的组件for letter in text: # 遍历输入文本的每个字符if mode == "textbox": # 如果用户选择的是文本框模式components.append(gr.Textbox(value=letter)) # 则为每个字符创建一个Textbox组件,并将其添加到列表中else: # 如果用户选择的是按钮模式components.append(gr.Button(letter)) # 则为每个字符创建一个Button组件,并将其添加到列表中return components # 返回包含所有根据模式生成的组件的列表demo.launch() # 启动应用
这段代码使用Gradio库创建了一个Web应用程序,允许用户输入一个字符串并选择输出模式(文本框或按钮)。应用将根据用户输入的字符串,针对每个字符动态创建并展示对应的组件,这些组件类型取决于用户通过单选按钮选择的模式。
input_text
是用户输入字符串的文本框。mode
是一个单选按钮组,允许用户选择输出模式,即是显示文本框还是按钮。show_split
函数根据用户输入的字符串和所选择的模式动态生成组件。如果字符串为空,则显示一个Markdown组件提醒用户“没有提供输入”;否则,根据选择的模式为字符串中的每个字符生成对应的文本框或按钮。
这个示例展示了Gradio中如何处理复杂的条件逻辑来动态渲染不同种类的组件,以及如何基于用户的选择和输入来调整应用界面的展示内容,提高用户交互体验。
默认情况下, @gr.render
重新运行是由应用程序的 .load
监听器和任何提供的输入组件的 .change
监听器触发的。我们可以通过在装饰器中显式设置触发器来覆盖这一点,就像我们在这个应用程序中所做的那样,只在 input_text.submit
上触发。如果您正在设置自定义触发器,并且还希望在应用程序开始时自动渲染,请确保将 demo.load
添加到您的触发器列表中。
动态事件监听器
如果您正在创建组件,您可能也想为它们附加事件监听器。让我们看一个例子,它接受可变数量的文本框作为输入,并将所有文本合并到一个框中。
import gradio as gr # 导入gradio库with gr.Blocks() as demo: # 使用gr.Blocks创建一个应用布局text_count = gr.State(1) # 使用状态变量记录文本框的数量,初始为1add_btn = gr.Button("Add Box") # 创建一个按钮,用于增加文本框add_btn.click(lambda x: x + 1, text_count, text_count) # 将按钮点击事件绑定到lambda函数,每次点击时将文本框数量加1# 定义函数,根据当前文本框数量动态生成文本框@gr.render(inputs=text_count)def render_count(count):boxes = [] # 初始化一个列表,用于存放动态生成的文本框for i in range(count): # 根据文本框的数量循环生成文本框box = gr.Textbox(key=i, label=f"Box {i}") # 为每个文本框设置唯一的key和标签boxes.append(box) # 将生成的文本框添加到列表中# 定义函数,用于合并所有文本框中的文本def merge(*args):return " ".join(args) # 将所有文本框的内容用空格连接起来merge_btn.click(merge, boxes, output) # 将"Merge"按钮绑定到merge函数,将所有文本框的内容合并并显示在输出框中merge_btn = gr.Button("Merge") # 创建一个用于触发合并操作的按钮output = gr.Textbox(label="Merged Output") # 创建一个文本框,用于显示合并后的文本demo.launch() # 启动应用
这段代码展示了如何使用Gradio库创建一个动态的Web应用程序,它允许用户通过点击按钮动态增加文本框,并且提供了一个合并按钮来合并所有文本框中的内容。
应用程序初始时,有一个文本框和两个按钮:一个用于增加文本框的 "Add Box" 按钮,另一个用于合并所有文本框内容的 "Merge" 按钮。
每次用户点击 "Add Box" 按钮,通过修改状态变量
text_count
来记录当前文本框的数量,随后动态在界面上渲染相应数量的文本框。"Merge" 按钮绑定了一个函数,该函数将所有创建的文本框中的文本合并,并将合并后的结果显示在一个标记为 "Merged Output" 的输出文本框中。
让我们看看这里发生了什么:
状态变量
text_count
用于跟踪要创建的文本框数量。通过点击添加按钮,我们增加text_count
,这触发了渲染装饰器。请注意,在我们在渲染函数中创建的每一个文本框中,我们都明确设置了一个
key=
参数。这个键允许我们在重新渲染之间保留这个组件的值。如果您在文本框中输入一个值,然后点击添加按钮,所有的文本框都会重新渲染,但它们的值不会被清除,因为key=
保持了一个组件在渲染过程中的值。我们已经将创建的文本框存储在一个列表中,并将这个列表作为输入提供给合并按钮事件监听器。请注意,所有使用在渲染函数内部创建的组件的事件监听器也必须在该渲染函数内部定义。事件监听器仍然可以引用渲染函数外部的组件,就像我们在这里所做的,通过引用
merge_btn
和output
,它们都是在渲染函数外部定义的。
正如组件一样,每当函数重新渲染时,前一次渲染创建的事件监听器会被清除,最新运行的新事件监听器会被附加上。
这使我们能够创建高度可定制和复杂的互动!
整合在一起 Putting it Together
让我们来看两个使用以上所有功能的例子。首先,尝试下面的待办事项应用程序:
import gradio as gr # 导入gradio库with gr.Blocks() as demo: # 开启一个使用Blocks的Gradio应用布局tasks = gr.State([]) # 使用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}], "" # 在任务清单中添加一项新任务,并重置输入框# 将新任务的文本框的提交动作绑定到add_task函数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"] = Truereturn task_listdone_btn.click(mark_done, None, [tasks]) # 绑定按钮点击到mark_done函数# 创建删除任务的按钮delete_btn = gr.Button("Delete", scale=0, variant="stop")def delete(task=task): # 定义删除任务的函数task_list.remove(task)return task_listdelete_btn.click(delete, None, [tasks]) # 绑定按钮点击到delete函数# 显示已完成任务的标题和数量gr.Markdown(f"### Complete Tasks ({len(complete)})")for task in complete:gr.Textbox(task['name'], show_label=False, container=False) # 为每个已完成的任务显示一个文本框以展示任务名称demo.launch() # 启动Gradio应用
这段代码通过Gradio库实现了一个简单的待办事项(To-Do List)应用。用户可以输入新的待办任务名称,待办任务将被添加到清单中并显示在页面上。每个待办事项旁边有两个按钮:“Done”和“Delete”。点击"Done"按钮将标记任务为已完成,点击"Delete"按钮则将任务从清单中删除。整个待办事项清单被分成两部分显示:未完成的任务和已完成的任务。
此应用主要展示了Gradio的State使用来存储和更新任务清单,以及如何使用Buttons和Textboxes创建交互式界面。这是一个典型的示例,展示了通过简单的Gradio界面实现复杂的应用逻辑。
请注意,几乎整个应用程序都在一个单一的 gr.render
内部,该 gr.render
会对任务 gr.State
变量做出反应。这个变量是一个嵌套列表,这带来了一些复杂性。如果您设计一个 gr.render
来响应列表或字典结构,请确保执行以下操作:
任何修改状态变量的事件监听器,如果这种修改应该触发重新渲染,必须将状态变量设置为输出。这让 Gradio 知道要检查变量是否在后台发生了变化。
在一个
gr.render
中,如果循环中的变量在事件监听函数内部使用,那么这个变量应该通过在函数头部将其设置为默认参数来“冻结”。看看我们是如何在mark_done
和delete
中都有task=task
。这将变量冻结为其“循环时”的值。
让我们来看一个最后的例子,它使用了我们学到的所有东西。下面是一个音频混音器。提供多个音轨并将它们混合在一起。
这段代码通过Gradio库实现了一个音轨合并应用。用户可以通过点击"Add Track"按钮来动态增加需要合并的音轨数量。对于每个音轨,用户可以上传音频文件并设置音量。应用中提供了"Merge Tracks"按钮来启动音轨合并的操作,合并后的音轨将会在页面下方的"Output"音频播放器中播放。
具体实现包括:
使用
State
记录音轨数量,初始值为1。通过点击“Add Track”按钮,动态增加音轨数量。
对于每个音轨,提供了文件上传(
Audio
)、音轨名称(Textbox
)和音量控制(Slider
)的功能。"Merge Tracks"按钮绑定的
merge
函数处理所有音轨的合并逻辑。该函数遍历每个音轨,根据其音量调整音频数据,然后将所有音轨数据进行合并。合并音轨时通过NumPy处理音频数据,包括调整音量和音轨长度匹配,以及将多个音轨叠加。
最终合并后的音轨将在"Output"音频播放器中播放给用户听。
注意:为确保代码正确执行,merge_btn.click(merge, set(audios + volumes), output_audio)
这行代码应放在所有组件(特别是audios
和 volumes
列表)完全定义之后。
import gradio as gr # 导入Gradio库
import numpy as np # 导入NumPy库,用于处理音频数据with gr.Blocks() as demo: # 使用gr.Blocks创建一个应用布局track_count = gr.State(1) # 使用State记录音轨数量,默认为1add_track_btn = gr.Button("Add Track") # 创建一个按钮,用于增加音轨数量# 将按钮点击事件绑定到一个lambda函数,每次点击时将音轨数量加1add_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, Nonefor 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函数merge_btn = gr.Button("Merge Tracks") # 创建一个按钮,用于启动音轨合并操作output_audio = gr.Audio(label="Output", interactive=False) # 创建一个音频输出组件,用于播放合并后的音轨demo.launch() # 启动应用
在这个应用程序中需要注意两件事:
我们为所有组件提供了key=!我们需要这样做,以便在为现有轨道设置值之后添加另一个轨道时,我们对现有轨道的输入值在重新渲染时不会被重置。
当传递给事件监听器的组件类型多样且数量任意时,使用集合和字典表示法输入比使用列表表示法更为简便。如上所述,我们在将输入传递给合并函数时,制作了一个包含所有输入gr.Audio和gr.Slider组件的大集合。在函数体中,我们将组件值作为字典查询。
gr.render大大扩展了gradio的功能 - 看看你能从中创造出什么!