Introducing Gradio 5.0
Read MoreIntroducing Gradio 5.0
Read MoreThe components and event listeners you define in a Blocks so far have been fixed - once the demo was launched, new components and listeners could not be added, and existing one could not be removed.
The @gr.render
decorator introduces the ability to dynamically change this. Let's take a look.
In the example below, we will create a variable number of Textboxes. When the user edits the input Textbox, we create a Textbox for each letter in the input. Try it out below:
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()
See how we can now create a variable number of Textboxes using our custom logic - in this case, a simple for
loop. The @gr.render
decorator enables this with the following steps:
inputs=
argument of @gr.render, and create a corresponding argument in your function for each component. This function will automatically re-run on any change to a component.Now whenever the inputs change, the function re-runs, and replaces the components created from the previous function run with the latest run. Pretty straightforward! Let's add a little more complexity to this app:
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()
By default, @gr.render
re-runs are triggered by the .load
listener to the app and the .change
listener to any input component provided. We can override this by explicitly setting the triggers in the decorator, as we have in this app to only trigger on input_text.submit
instead.
If you are setting custom triggers, and you also want an automatic render at the start of the app, make sure to add demo.load
to your list of triggers.
If you're creating components, you probably want to attach event listeners to them as well. Let's take a look at an example that takes in a variable number of Textbox as input, and merges all the text into a single box.
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()
Let's take a look at what's happening here:
text_count
is keeping track of the number of Textboxes to create. By clicking on the Add button, we increase text_count
which triggers the render decorator.key=
argument. This key allows us to preserve the value of this Component between re-renders. If you type in a value in a textbox, and then click the Add button, all the Textboxes re-render, but their values aren't cleared because the key=
maintains the the value of a Component across a render.merge_btn
and output
which are both defined outside the render function.Just as with Components, whenever a function re-renders, the event listeners created from the previous render are cleared and the new event listeners from the latest run are attached.
This allows us to create highly customizable and complex interactions!
Let's look at two examples that use all the features above. First, try out the to-do list app below:
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()
Note that almost the entire app is inside a single gr.render
that reacts to the tasks gr.State
variable. This variable is a nested list, which presents some complexity. If you design a gr.render
to react to a list or dict structure, ensure you do the following:
gr.render
, if a variable in a loop is used inside an event listener function, that variable should be "frozen" via setting it to itself as a default argument in the function header. See how we have task=task
in both mark_done
and delete
. This freezes the variable to its "loop-time" value.Let's take a look at one last example that uses everything we learned. Below is an audio mixer. Provide multiple audio tracks and mix them together.
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()
Two things to note in this app:
key=
to all the components! We need to do this so that if we add another track after setting the values for an existing track, our input values to the existing track do not get reset on re-render.gr.Audio
and gr.Slider
components when we pass the inputs to the merge
function. In the function body we query the component values as a dict.The gr.render
expands gradio capabilities extensively - see what you can make out of it!