diff --git a/.changeset/neat-cobras-lie.md b/.changeset/neat-cobras-lie.md new file mode 100644 index 0000000000000..c4fd67b2ebfda --- /dev/null +++ b/.changeset/neat-cobras-lie.md @@ -0,0 +1,7 @@ +--- +"@gradio/client": minor +"@gradio/core": minor +"gradio": minor +--- + +feat:Allow showing progress updates on arbitrary components diff --git a/client/js/src/types.ts b/client/js/src/types.ts index f528a58d3ee8f..2694bc86ed9b4 100644 --- a/client/js/src/types.ts +++ b/client/js/src/types.ts @@ -249,6 +249,7 @@ export interface Dependency { trigger: "click" | "load" | string; max_batch_size: number; show_progress: "full" | "minimal" | "hidden"; + show_progress_on: number[] | null; frontend_fn: ((...args: unknown[]) => Promise) | null; status?: string; queue: boolean | null; diff --git a/demo/cancel_events/run.ipynb b/demo/cancel_events/run.ipynb index 82e9c09d6b630..df566ca57e94d 100644 --- a/demo/cancel_events/run.ipynb +++ b/demo/cancel_events/run.ipynb @@ -1 +1 @@ -{"cells": [{"cell_type": "markdown", "id": "302934307671667531413257853548643485645", "metadata": {}, "source": ["# Gradio Demo: cancel_events"]}, {"cell_type": "code", "execution_count": null, "id": "272996653310673477252411125948039410165", "metadata": {}, "outputs": [], "source": ["!pip install -q gradio "]}, {"cell_type": "code", "execution_count": null, "id": "288918539441861185822528903084949547379", "metadata": {}, "outputs": [], "source": ["import time\n", "import gradio as gr\n", "import atexit\n", "import pathlib\n", "\n", "log_file = pathlib.Path(__file__).parent / \"cancel_events_output_log.txt\"\n", "\n", "def fake_diffusion(steps):\n", " log_file.write_text(\"\")\n", " for i in range(steps):\n", " print(f\"Current step: {i}\")\n", " with log_file.open(\"a\") as f:\n", " f.write(f\"Current step: {i}\\n\")\n", " time.sleep(0.2)\n", " yield str(i)\n", "\n", "def long_prediction(*args, **kwargs):\n", " time.sleep(10)\n", " return 42\n", "\n", "with gr.Blocks() as demo:\n", " with gr.Row():\n", " with gr.Column():\n", " n = gr.Slider(1, 10, value=9, step=1, label=\"Number Steps\")\n", " run = gr.Button(value=\"Start Iterating\")\n", " output = gr.Textbox(label=\"Iterative Output\")\n", " stop = gr.Button(value=\"Stop Iterating\")\n", " with gr.Column():\n", " textbox = gr.Textbox(label=\"Prompt\")\n", " prediction = gr.Number(label=\"Expensive Calculation\")\n", " run_pred = gr.Button(value=\"Run Expensive Calculation\")\n", " with gr.Column():\n", " cancel_on_change = gr.Textbox(\n", " label=\"Cancel Iteration and Expensive Calculation on Change\"\n", " )\n", " cancel_on_submit = gr.Textbox(\n", " label=\"Cancel Iteration and Expensive Calculation on Submit\"\n", " )\n", " echo = gr.Textbox(label=\"Echo\")\n", " with gr.Row():\n", " with gr.Column():\n", " image = gr.Image(\n", " sources=[\"webcam\"], label=\"Cancel on clear\", interactive=True\n", " )\n", " with gr.Column():\n", " video = gr.Video(\n", " sources=[\"webcam\"], label=\"Cancel on start recording\", interactive=True\n", " )\n", "\n", " click_event = run.click(fake_diffusion, n, output)\n", " stop.click(fn=None, inputs=None, outputs=None, cancels=[click_event])\n", " pred_event = run_pred.click(\n", " fn=long_prediction, inputs=[textbox], outputs=prediction\n", " )\n", "\n", " cancel_on_change.change(None, None, None, cancels=[click_event, pred_event])\n", " cancel_on_submit.submit(\n", " lambda s: s, cancel_on_submit, echo, cancels=[click_event, pred_event]\n", " )\n", " image.clear(None, None, None, cancels=[click_event, pred_event])\n", " video.start_recording(None, None, None, cancels=[click_event, pred_event])\n", "\n", " demo.queue(max_size=20)\n", " atexit.register(lambda: log_file.unlink())\n", "\n", "if __name__ == \"__main__\":\n", " demo.launch()\n"]}], "metadata": {}, "nbformat": 4, "nbformat_minor": 5} \ No newline at end of file +{"cells": [{"cell_type": "markdown", "id": "302934307671667531413257853548643485645", "metadata": {}, "source": ["# Gradio Demo: cancel_events"]}, {"cell_type": "code", "execution_count": null, "id": "272996653310673477252411125948039410165", "metadata": {}, "outputs": [], "source": ["!pip install -q gradio "]}, {"cell_type": "code", "execution_count": null, "id": "288918539441861185822528903084949547379", "metadata": {}, "outputs": [], "source": ["import time\n", "import gradio as gr\n", "import atexit\n", "import pathlib\n", "\n", "log_file = pathlib.Path(__file__).parent / \"cancel_events_output_log.txt\"\n", "\n", "def fake_diffusion(steps):\n", " log_file.write_text(\"\")\n", " for i in range(steps):\n", " print(f\"Current step: {i}\")\n", " with log_file.open(\"a\") as f:\n", " f.write(f\"Current step: {i}\\n\")\n", " time.sleep(0.2)\n", " yield str(i)\n", "\n", "def long_prediction(*args, **kwargs):\n", " time.sleep(4)\n", " return 42, 42\n", "\n", "with gr.Blocks() as demo:\n", " with gr.Row():\n", " with gr.Column():\n", " n = gr.Slider(1, 10, value=9, step=1, label=\"Number Steps\")\n", " run = gr.Button(value=\"Start Iterating\")\n", " output = gr.Textbox(label=\"Iterative Output\")\n", " stop = gr.Button(value=\"Stop Iterating\")\n", " with gr.Column():\n", " textbox = gr.Textbox(label=\"Prompt\")\n", " loading_box = gr.Textbox(label=\"Loading indicator for expensive calculation\")\n", " loading_box2 = gr.Textbox(label=\"Loading indicator for expensive calculation\")\n", " prediction = gr.Number(label=\"Expensive Calculation\")\n", " prediction2 = gr.Number(label=\"Expensive Calculation\")\n", " run_pred = gr.Button(value=\"Run Expensive Calculation\")\n", " with gr.Column():\n", " cancel_on_change = gr.Textbox(\n", " label=\"Cancel Iteration and Expensive Calculation on Change\"\n", " )\n", " cancel_on_submit = gr.Textbox(\n", " label=\"Cancel Iteration and Expensive Calculation on Submit\"\n", " )\n", " echo = gr.Textbox(label=\"Echo\")\n", " with gr.Row():\n", " with gr.Column():\n", " image = gr.Image(\n", " sources=[\"webcam\"], label=\"Cancel on clear\", interactive=True\n", " )\n", " with gr.Column():\n", " video = gr.Video(\n", " sources=[\"webcam\"], label=\"Cancel on start recording\", interactive=True\n", " )\n", "\n", " click_event = run.click(fake_diffusion, n, output)\n", " stop.click(fn=None, inputs=None, outputs=None, cancels=[click_event])\n", " pred_event = run_pred.click(\n", " fn=long_prediction, inputs=[textbox], outputs=[prediction, prediction2], show_progress_on=[loading_box, loading_box2]\n", " )\n", "\n", " cancel_on_change.change(None, None, None, cancels=[click_event, pred_event])\n", " cancel_on_submit.submit(\n", " lambda s: s, cancel_on_submit, echo, cancels=[click_event, pred_event]\n", " )\n", " image.clear(None, None, None, cancels=[click_event, pred_event])\n", " video.start_recording(None, None, None, cancels=[click_event, pred_event])\n", "\n", " demo.queue(max_size=20)\n", " atexit.register(lambda: log_file.unlink())\n", "\n", "if __name__ == \"__main__\":\n", " demo.launch()\n"]}], "metadata": {}, "nbformat": 4, "nbformat_minor": 5} \ No newline at end of file diff --git a/demo/cancel_events/run.py b/demo/cancel_events/run.py index d7c2f5700d89f..9457f442cc368 100644 --- a/demo/cancel_events/run.py +++ b/demo/cancel_events/run.py @@ -15,8 +15,8 @@ def fake_diffusion(steps): yield str(i) def long_prediction(*args, **kwargs): - time.sleep(10) - return 42 + time.sleep(4) + return 42, 42 with gr.Blocks() as demo: with gr.Row(): @@ -27,7 +27,10 @@ def long_prediction(*args, **kwargs): stop = gr.Button(value="Stop Iterating") with gr.Column(): textbox = gr.Textbox(label="Prompt") + loading_box = gr.Textbox(label="Loading indicator for expensive calculation") + loading_box2 = gr.Textbox(label="Loading indicator for expensive calculation") prediction = gr.Number(label="Expensive Calculation") + prediction2 = gr.Number(label="Expensive Calculation") run_pred = gr.Button(value="Run Expensive Calculation") with gr.Column(): cancel_on_change = gr.Textbox( @@ -50,7 +53,7 @@ def long_prediction(*args, **kwargs): click_event = run.click(fake_diffusion, n, output) stop.click(fn=None, inputs=None, outputs=None, cancels=[click_event]) pred_event = run_pred.click( - fn=long_prediction, inputs=[textbox], outputs=prediction + fn=long_prediction, inputs=[textbox], outputs=[prediction, prediction2], show_progress_on=[loading_box, loading_box2] ) cancel_on_change.change(None, None, None, cancels=[click_event, pred_event]) diff --git a/gradio/blocks.py b/gradio/blocks.py index 278661c3526ff..14979cbe8f71d 100644 --- a/gradio/blocks.py +++ b/gradio/blocks.py @@ -511,6 +511,7 @@ def __init__( api_name: str | Literal[False] = False, js: str | None = None, show_progress: Literal["full", "minimal", "hidden"] = "full", + show_progress_on: Sequence[Component] | None = None, cancels: list[int] | None = None, collects_event_data: bool = False, trigger_after: int | None = None, @@ -548,6 +549,7 @@ def __init__( self.api_name = api_name self.js = js self.show_progress = show_progress + self.show_progress_on = show_progress_on self.cancels = cancels or [] self.collects_event_data = collects_event_data self.trigger_after = trigger_after @@ -605,6 +607,9 @@ def get_config(self): "api_name": self.api_name, "scroll_to_output": self.scroll_to_output, "show_progress": self.show_progress, + "show_progress_on": None + if self.show_progress_on is None + else [block._id for block in self.show_progress_on], "batch": self.batch, "max_batch_size": self.max_batch_size, "cancels": self.cancels, @@ -708,6 +713,7 @@ def set_event_trigger( postprocess: bool = True, scroll_to_output: bool = False, show_progress: Literal["full", "minimal", "hidden"] = "full", + show_progress_on: Component | Sequence[Component] | None = None, api_name: str | None | Literal[False] = None, js: str | None = None, no_target: bool = False, @@ -741,6 +747,7 @@ def set_event_trigger( postprocess: whether to run the postprocess methods of the output components after running the function scroll_to_output: whether to scroll to output of dependency on trigger show_progress: how to show the progress animation while event is running: "full" shows a spinner which covers the output component area as well as a runtime display in the upper right corner, "minimal" only shows the runtime display, "hidden" shows no progress animation at all + show_progress_on: Component or list of components to show the progress animation on. If None, will show the progress animation on all of the output components. api_name: defines how the endpoint appears in the API docs. Can be a string, None, or False. If set to a string, the endpoint will be exposed in the API docs with the given name. If None (default), the name of the function will be used as the API endpoint. If False, the endpoint will not be exposed in the API docs and downstream apps (including those that `gr.load` this app) will not be able to use this event. js: Optional frontend js method to run before running 'fn'. Input arguments for js method are values of 'inputs' and 'outputs', return should be a list of values for output components no_target: if True, sets "targets" to [], used for the Blocks.load() event and .then() events @@ -785,6 +792,8 @@ def set_event_trigger( outputs = [] elif not isinstance(outputs, Sequence): outputs = [outputs] + if show_progress_on and not isinstance(show_progress_on, Sequence): + show_progress_on = [show_progress_on] if fn is not None and not cancels: check_function_inputs_match(fn, inputs, inputs_as_dict) @@ -863,6 +872,7 @@ def set_event_trigger( api_name=api_name, js=js, show_progress=show_progress, + show_progress_on=show_progress_on, cancels=cancels, collects_event_data=collects_event_data, trigger_after=trigger_after, diff --git a/gradio/component_meta.py b/gradio/component_meta.py index 75591bd5fb5f2..6065d2e22423d 100644 --- a/gradio/component_meta.py +++ b/gradio/component_meta.py @@ -27,6 +27,7 @@ def {{ event.event_name }}(self, api_name: str | None | Literal[False] = None, scroll_to_output: bool = False, show_progress: Literal["full", "minimal", "hidden"] = "full", + show_progress_on: Component | Sequence[Component] | None = None, queue: bool | None = None, batch: bool = False, max_batch_size: int = 4, @@ -51,6 +52,7 @@ def {{ event.event_name }}(self, api_name: defines how the endpoint appears in the API docs. Can be a string, None, or False. If False, the endpoint will not be exposed in the api docs. If set to None, will use the functions name as the endpoint route. If set to a string, the endpoint will be exposed in the api docs with the given name. scroll_to_output: if True, will scroll to output component on completion show_progress: how to show the progress animation while event is running: "full" shows a spinner which covers the output component area as well as a runtime display in the upper right corner, "minimal" only shows the runtime display, "hidden" shows no progress animation at all + show_progress_on: Component or list of components to show the progress animation on. If None, will show the progress animation on all of the output components. queue: if True, will place the request on the queue, if the queue has been enabled. If False, will not put this event on the queue, even if the queue has been enabled. If None, will use the queue setting of the gradio app. batch: if True, then the function should process a batch of inputs, meaning that it should accept a list of input values for each parameter. The lists should be of equal length (and be up to length `max_batch_size`). The function is then *required* to return a tuple of lists (even if there is only 1 output component), with each list in the tuple corresponding to one output component. max_batch_size: maximum number of inputs to batch together if this is called from the queue (only relevant if batch=True) diff --git a/gradio/events.py b/gradio/events.py index 7efb9ff825622..d72b419b2070b 100644 --- a/gradio/events.py +++ b/gradio/events.py @@ -474,6 +474,7 @@ class EventListenerMethod: Union[str, None, Literal[False]], bool, Literal["full", "minimal", "hidden"], + Union[Component, Sequence[Component], None], Union[bool, None], bool, int, @@ -578,6 +579,7 @@ def event_trigger( api_name: str | None | Literal[False] = None, scroll_to_output: bool = False, show_progress: Literal["full", "minimal", "hidden"] = _show_progress, + show_progress_on: Component | Sequence[Component] | None = None, queue: bool = True, batch: bool = False, max_batch_size: int = 4, @@ -601,6 +603,7 @@ def event_trigger( api_name: defines how the endpoint appears in the API docs. Can be a string, None, or False. If set to a string, the endpoint will be exposed in the API docs with the given name. If None (default), the name of the function will be used as the API endpoint. If False, the endpoint will not be exposed in the API docs and downstream apps (including those that `gr.load` this app) will not be able to use this event. scroll_to_output: If True, will scroll to output component on completion show_progress: how to show the progress animation while event is running: "full" shows a spinner which covers the output component area as well as a runtime display in the upper right corner, "minimal" only shows the runtime display, "hidden" shows no progress animation at all + show_progress_on: Component or list of components to show the progress animation on. If None, will show the progress animation on all of the output components. queue: If True, will place the request on the queue, if the queue has been enabled. If False, will not put this event on the queue, even if the queue has been enabled. If None, will use the queue setting of the gradio app. batch: If True, then the function should process a batch of inputs, meaning that it should accept a list of input values for each parameter. The lists should be of equal length (and be up to length `max_batch_size`). The function is then *required* to return a tuple of lists (even if there is only 1 output component), with each list in the tuple corresponding to one output component. max_batch_size: Maximum number of inputs to batch together if this is called from the queue (only relevant if batch=True) @@ -625,6 +628,7 @@ def wrapper(func): api_name=api_name, scroll_to_output=scroll_to_output, show_progress=show_progress, + show_progress_on=show_progress_on, queue=queue, batch=batch, max_batch_size=max_batch_size, @@ -672,6 +676,7 @@ def inner(*args, **kwargs): postprocess=postprocess, scroll_to_output=scroll_to_output, show_progress=show_progress, + show_progress_on=show_progress_on, api_name=api_name, js=js, concurrency_limit=concurrency_limit, @@ -738,6 +743,7 @@ def on( api_name: str | None | Literal[False] = None, scroll_to_output: bool = False, show_progress: Literal["full", "minimal", "hidden"] = "full", + show_progress_on: Component | Sequence[Component] | None = None, queue: bool = True, batch: bool = False, max_batch_size: int = 4, @@ -764,7 +770,8 @@ def on( outputs: List of gradio.components to use as outputs. If the function returns no outputs, this should be an empty list. api_name: Defines how the endpoint appears in the API docs. Can be a string, None, or False. If False, the endpoint will not be exposed in the api docs. If set to None, will use the functions name as the endpoint route. If set to a string, the endpoint will be exposed in the api docs with the given name. scroll_to_output: If True, will scroll to output component on completion - show_progress: how to show the progress animation while event is running: "full" shows a spinner which covers the output component area as well as a runtime display in the upper right corner, "minimal" only shows the runtime display, "hidden" shows no progress animation at all + show_progress: how to show the progress animation while event is running: "full" shows a spinner which covers the output component area as well as a runtime display in the upper right corner, "minimal" only shows the runtime display, "hidden" shows no progress animation at all, + show_progress_on: Component or list of components to show the progress animation on. If None, will show the progress animation on all of the output components. queue: If True, will place the request on the queue, if the queue has been enabled. If False, will not put this event on the queue, even if the queue has been enabled. If None, will use the queue setting of the gradio app. batch: If True, then the function should process a batch of inputs, meaning that it should accept a list of input values for each parameter. The lists should be of equal length (and be up to length `max_batch_size`). The function is then *required* to return a tuple of lists (even if there is only 1 output component), with each list in the tuple corresponding to one output component. max_batch_size: Maximum number of inputs to batch together if this is called from the queue (only relevant if batch=True) @@ -813,6 +820,7 @@ def wrapper(func): api_name=api_name, scroll_to_output=scroll_to_output, show_progress=show_progress, + show_progress_on=show_progress_on, queue=queue, batch=batch, max_batch_size=max_batch_size, @@ -864,6 +872,7 @@ def inner(*args, **kwargs): postprocess=postprocess, scroll_to_output=scroll_to_output, show_progress=show_progress, + show_progress_on=show_progress_on, api_name=api_name, js=js, concurrency_limit=concurrency_limit, diff --git a/js/core/src/init.ts b/js/core/src/init.ts index cf264449438df..f8d462a35e9a4 100644 --- a/js/core/src/init.ts +++ b/js/core/src/init.ts @@ -131,7 +131,11 @@ export function create_components(initial_layout: ComponentMeta | undefined): { components.push(_rootNode); dependencies.forEach((dep) => { - loading_status.register(dep.id, dep.inputs, dep.outputs); + loading_status.register( + dep.id, + dep.inputs, + dep.show_progress_on || dep.outputs + ); dep.frontend_fn = process_frontend_fn( dep.js, !!dep.backend_fn, diff --git a/js/core/src/types.ts b/js/core/src/types.ts index 07827c74ae1a6..832f1c8b11ab3 100644 --- a/js/core/src/types.ts +++ b/js/core/src/types.ts @@ -55,6 +55,7 @@ export interface Dependency { js: string | null; scroll_to_output: boolean; show_progress: "full" | "minimal" | "hidden"; + show_progress_on: number[] | null; frontend_fn: ((...args: unknown[]) => Promise) | null; status?: string; queue: boolean | null;