Closed aliabid94 closed 3 weeks ago
• | Name | Status | URL |
---|---|---|---|
Spaces | ready! | Spaces preview | |
Website | ready! | Website preview | |
Storybook | ready! | Storybook preview | |
:unicorn: | Changes | detected! | Details |
Install Gradio from this PR
pip install https://gradio-builds.s3.amazonaws.com/6cb2f6e95867ff849650e1378538141f1bba4c5b/gradio-4.31.5-py3-none-any.whl
Install Gradio Python Client from this PR
pip install "gradio-client @ git+https://github.com/gradio-app/gradio@6cb2f6e95867ff849650e1378538141f1bba4c5b#subdirectory=client/python"
Package | Version |
---|---|
@gradio/app |
patch |
@gradio/client |
patch |
gradio |
patch |
gradio_client |
patch |
Add event listener support to render blocks
Maintainers or the PR author can modify the PR title to modify this entry.
Awesome work! Any chance I can get the pip download like last time or is the release coming soon?
@jameszhou02 You can try:
I'm seeing the same behavior that @freddyaboulton described, where the event does not happen at the first trigger, but happens at the second trigger and subsequent triggers. For example, in this demo:
import random
with gr.Blocks() as demo:
t = gr.Textbox()
@gr.render(triggers=[t.submit])
def inference():
with gr.Row():
for i in range(random.randint(3, 8)):
gr.Audio()
if __name__ == "__main__":
demo.launch()
When I first submit the textbox, nothing happens, but each subsequent submit works.
It seems that the load()
event cannot be used as a trigger in gr.render
-- edit: I think its the same bug as above since the event isn't being triggered the first time:
import random
import gradio as gr
with gr.Blocks() as demo:
@gr.render(triggers=[demo.load])
def inference():
for i in range(random.randint(3, 8)):
gr.Audio()
if __name__ == "__main__":
demo.launch()
When we write docs for this, we should clarify that any event listeners that use rendered components must be inside the render function. For example, this works:
import random
import gradio as gr
with gr.Blocks() as demo:
@gr.render(triggers=[demo.load])
def inference():
audios = []
for i in range(random.randint(3, 8)):
audios.append(gr.Audio())
count.click(lambda *args:len(args), audios, number)
count = gr.Button("Count")
number = gr.Number()
if __name__ == "__main__":
demo.launch()
but a user might write this, which doesn't:
import random
import gradio as gr
audios = []
with gr.Blocks() as demo:
@gr.render(triggers=[demo.load])
def inference():
audios = []
for i in range(random.randint(3, 8)):
audios.append(gr.Audio())
count = gr.Button("Count")
number = gr.Number()
count.click(lambda *args:len(args), audios, number)
if __name__ == "__main__":
demo.launch()
Didn't find anything else, great work @aliabid94!!
Saw a weird behavior with reload mode where updating an input to the slider caused the render to trigger
Actually a general bug with reload mode that all change listeners trigger on a refresh. Since render is listening to slider.change, it gets triggered. Separate bug I can tackle separately but not too high priority.
Sometimes the render does not trigger? When I update the slider, the event does not run. Doesn't happen all the time though.
Fixed!
You cannot define an event which skips the queue from within a render. If that event is triggered, an error happens.
Fixed!
It seems that the load() event cannot be used as a trigger in gr.render -- edit: I think its the same bug as above since the event isn't being triggered the first time:
Fixed!
Should be ready to merge if there are no other issues
Nice @aliabid94! I'll give this another pass to ensure that this works. Can we add docs for this as part of this PR so that we can fully release this feature once this PR is merged in? (Btw we can update or remove this section in the docs: https://www.gradio.app/guides/controlling-layout#variable-number-of-outputs). I've added a bunch of issues that this closes to the parent commment
Actually 2 small things I noticed:
triggers
parameter in gr.render()
requires a list (even though inputs
and outputs
don't) --> it should convert singletons to lists automaticallygr.render()
E.g. the slider here is not interactive automatically;
import gradio as gr
with gr.Blocks() as demo:
s = gr.Slider(1, 4, step=1)
@gr.render(inputs=s, triggers=[s.change])
def inference(num):
with gr.Row():
for i in range(num):
gr.Textbox()
demo.launch()
One other thing I noticed was that a @gr.render
automatically creates a gr.Column
for the rendered components, even if the parent BlocksContext is a gr.Row
.
I.e. this does not work as expected:
import gradio as gr
with gr.Blocks() as demo:
b = gr.Button("Add Textbox")
num = gr.State(0)
b.click(lambda x:x+1, num, num)
with gr.Row():
@gr.render(inputs=num)
def show_textbox(n):
for i in range(n):
gr.Textbox()
demo.launch()
(it renders the textboxes in a column). However, this works as expected:
import gradio as gr
with gr.Blocks() as demo:
b = gr.Button("Add Textbox")
num = gr.State(0)
b.click(lambda x:x+1, num, num)
@gr.render(inputs=num)
def show_textbox(n):
with gr.Row():
for i in range(n):
gr.Textbox()
demo.launch()
And one other thing that I noticed is that the trigger_mode="always_last"
behavior doesn't seem to apply to gr.render()
. Here's what I mean. I built a Space with this code:
import gradio as gr
with gr.Blocks() as demo:
gr.Markdown("# Train a Text Classifier with Synthetic Data")
labels = gr.Dropdown(choices=[], value=[], label="Classes", allow_custom_value=True, multiselect=True)
@gr.render(inputs=[labels])
def show_textbox(labels_):
with gr.Row():
for label in labels_:
gr.TextArea(label=f"Samples for class: {label}")
if len(labels_)>=2:
with gr.Row():
gr.Button("TRAIN!", variant="primary")
demo.launch()
If I quickly change the value of the dropdown by backspacing and deleting the choices, you can see that a couple of the textareas are still visible at the end:
https://github.com/gradio-app/gradio/assets/1778297/8d262d9f-5161-42bd-bb15-745231f71c4f
Thanks for the reviews! Will add docs, and some more tests and cleanup in follow up PR
And one other thing that I noticed is that the
trigger_mode="always_last"
behavior doesn't seem to apply togr.render()
. Here's what I mean. I built a Space with this code:import gradio as gr with gr.Blocks() as demo: gr.Markdown("# Train a Text Classifier with Synthetic Data") labels = gr.Dropdown(choices=[], value=[], label="Classes", allow_custom_value=True, multiselect=True) @gr.render(inputs=[labels]) def show_textbox(labels_): with gr.Row(): for label in labels_: gr.TextArea(label=f"Samples for class: {label}") if len(labels_)>=2: with gr.Row(): gr.Button("TRAIN!", variant="primary") demo.launch()
If I quickly change the value of the dropdown by backspacing and deleting the choices, you can see that a couple of the textareas are still visible at the end: Screen.Recording.2024-05-21.at.11.33.06.PM.mov
Awesome feature! I'm just wondering if there's a way to define the event listener outside gr.Blocks? This is crucial for readability in large applications.
Hi @acylam great point. One thing that you can do is import Blocks from other files and .render()
them (this .render() is unrelated to gr.render()
-- sorry!) inside the main Blocks object. Like this:
Say this is your utils.py
file:
import gradio as gr
with gr.Blocks() as func_blocks:
labels = gr.Dropdown(choices=[], value=[], label="Classes", allow_custom_value=True, multiselect=True)
@gr.render(inputs=[labels])
def show_textbox(labels_):
with gr.Row():
for label in labels_:
gr.TextArea(label=f"Samples for class: {label}")
if len(labels_)>=2:
with gr.Row():
gr.Button("TRAIN!", variant="primary")
and in your main app.py
file, you could do:
import gradio as gr
from utils import func_blocks
with gr.Blocks() as demo:
gr.Markdown("# Train a Text Classifier with Synthetic Data")
func_blocks.render()
demo.launch()
Does this work for you? cc @aliabid94 if he has any other recommendations
Hi @acylam great point. One thing that you can do is import Blocks from other files and
.render()
them (this .render() is unrelated togr.render()
-- sorry!) inside the main Blocks object. Like this:Say this is your
utils.py
file:import gradio as gr with gr.Blocks() as func_blocks: labels = gr.Dropdown(choices=[], value=[], label="Classes", allow_custom_value=True, multiselect=True) @gr.render(inputs=[labels]) def show_textbox(labels_): with gr.Row(): for label in labels_: gr.TextArea(label=f"Samples for class: {label}") if len(labels_)>=2: with gr.Row(): gr.Button("TRAIN!", variant="primary")
and in your main
app.py
file, you could do:import gradio as gr from utils import func_blocks with gr.Blocks() as demo: gr.Markdown("# Train a Text Classifier with Synthetic Data") func_blocks.render() demo.launch()
Does this work for you? cc @aliabid94 if he has any other recommendations
@abidlabs Thanks for the quick reply. This works, but I was hoping for a way to separate event handling from UI elements. Kind of like how you'd define the structure of the UI in HTML and the event handling logic in JavaScript in traditional web development? So ideally, gr.Markdown
and labels=gr.Dropdown
will be defined in one file, and show_text
will be defined in a separate file. I hope I'm making sense. It could also be I'm completely missing the design of gradio.
cc @aliabid94
I've searched high and low and I can't find the issue or notes anywhere. I did find this message in slack that I wrote a long time ago:
Eventually i envisage many kinds of custom components:
- fully custom with a new python + html/js/css
- either extending from existing gradio components or fully custom implementations
- pure frontend components that use some gradio python class. Basically a new frontend for existing components
- Pure python components.
- Something like the above, composition for components with a new interface
- or new python implementation for an existing component that will use an existing frontend. (Maybe is changes stuff like preprocessing or w/e)
This one seems relevant here:
composition of [python] components with a new interface
The idea being that you could somehow contain a component within a function or block and wrap the events (or just forward them) with a new interface for better abstraction.
The original use case was:
gr.Render
was but a dream back then but I could see the above approach working for render as well.
Imo it is very valuable for complex apps and probably the last missing piece in terms of making gradio a really expressive way of building complex UI.
This PR allows adding event listeners inside render blocks. Take a look at the code from demo/render_merge below:
This is done by removing previous event listeners from a render block and attaching the new event listeners on every re-render. A previous assumption is no longer true: that
dependencies
is a static list of event listeners for an app, and event listener can be identified by it'sfn_index
which is it's index within this list of listeners. We've replaced this with a concept of an id for each dependency instead of using indices, and different sessions are able to extend the default set of event listeners by adding new dependencies with different ids.Closes: https://github.com/gradio-app/gradio/issues/4689 Closes: https://github.com/gradio-app/gradio/issues/7739 Closes: https://github.com/gradio-app/gradio/issues/2570 Closes: https://github.com/gradio-app/gradio/issues/2107 (once https://github.com/gradio-app/gradio/pull/8297 is merged in)