From 748f6c19277471aaf8848db8141369f3af9a6326 Mon Sep 17 00:00:00 2001 From: pngwn Date: Thu, 18 Jul 2024 10:43:53 +0100 Subject: [PATCH 001/195] enter pre-release mode --- .changeset/pre.json | 74 +++++++++++++++++++++++++++++++++++ .github/workflows/publish.yml | 1 + 2 files changed, 75 insertions(+) create mode 100644 .changeset/pre.json diff --git a/.changeset/pre.json b/.changeset/pre.json new file mode 100644 index 0000000000000..3228d41bbe5c4 --- /dev/null +++ b/.changeset/pre.json @@ -0,0 +1,74 @@ +{ + "mode": "pre", + "tag": "beta", + "initialVersions": { + "@gradio/client": "1.3.0", + "gradio_client": "1.1.0", + "gradio": "4.38.1", + "@gradio/cdn-test": "0.0.1", + "@gradio/spaces-test": "0.0.1", + "website": "0.34.0", + "@gradio/accordion": "0.3.18", + "@gradio/annotatedimage": "0.6.13", + "@gradio/app": "1.38.1", + "@gradio/atoms": "0.7.6", + "@gradio/audio": "0.12.2", + "@gradio/box": "0.1.20", + "@gradio/button": "0.2.46", + "@gradio/chatbot": "0.12.1", + "@gradio/checkbox": "0.3.8", + "@gradio/checkboxgroup": "0.5.8", + "@gradio/code": "0.7.0", + "@gradio/colorpicker": "0.3.8", + "@gradio/column": "0.1.2", + "@gradio/dataframe": "0.8.13", + "@gradio/dataset": "0.2.0", + "@gradio/datetime": "0.0.2", + "@gradio/downloadbutton": "0.1.23", + "@gradio/dropdown": "0.7.8", + "@gradio/fallback": "0.3.8", + "@gradio/file": "0.8.5", + "@gradio/fileexplorer": "0.4.14", + "@gradio/form": "0.1.20", + "@gradio/gallery": "0.11.2", + "@gradio/group": "0.1.1", + "@gradio/highlightedtext": "0.7.2", + "@gradio/html": "0.3.1", + "@gradio/icons": "0.6.0", + "@gradio/image": "0.12.2", + "@gradio/imageeditor": "0.7.13", + "@gradio/json": "0.2.8", + "@gradio/label": "0.3.8", + "@gradio/lite": "4.38.1", + "@gradio/markdown": "0.8.1", + "@gradio/model3d": "0.11.0", + "@gradio/multimodaltextbox": "0.5.2", + "@gradio/number": "0.4.8", + "@gradio/paramviewer": "0.4.17", + "@gradio/plot": "0.6.0", + "@gradio/preview": "0.10.1", + "gradio_test": "0.5.0", + "@gradio/radio": "0.5.8", + "@gradio/row": "0.1.3", + "@gradio/simpledropdown": "0.2.8", + "@gradio/simpleimage": "0.6.2", + "@gradio/simpletextbox": "0.2.8", + "@gradio/slider": "0.4.8", + "@gradio/state": "0.1.0", + "@gradio/statustracker": "0.7.1", + "@gradio/storybook": "0.6.0", + "@gradio/tabitem": "0.2.12", + "@gradio/tabs": "0.2.11", + "@gradio/textbox": "0.6.7", + "@gradio/theme": "0.2.3", + "@gradio/timer": "0.3.0", + "@gradio/tooltip": "0.1.0", + "@gradio/tootils": "0.6.0", + "@gradio/upload": "0.11.5", + "@gradio/uploadbutton": "0.6.14", + "@gradio/utils": "0.5.1", + "@gradio/video": "0.9.2", + "@gradio/wasm": "0.11.0" + }, + "changesets": [] +} diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index bba1d1c0f5150..b7efdd9be9f1a 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -3,6 +3,7 @@ on: push: branches: - main + - 5.0-dev env: CI: true From 6e6818c3af836051fffdd070a9e33889b246186e Mon Sep 17 00:00:00 2001 From: Abubakar Abid Date: Thu, 18 Jul 2024 02:45:14 -0700 Subject: [PATCH 002/195] Remove deprecated parameters and classes for the 5.0 release (#8797) * 5.0 * add changeset * deprecate more * add changeset * lint * Update rotten-bears-bathe.md * Update icy-clocks-juggle.md * changes * Delete .changeset/icy-clocks-juggle.md * every * more deprecation * deprecate inits * fix * fix func * fix some tests * format * fix more tests * fixes --------- Co-authored-by: gradio-pr-bot --- .changeset/rotten-bears-bathe.md | 5 + client/python/test/conftest.py | 26 -- client/python/test/test_client.py | 17 +- demo/file_explorer_component_events/run.ipynb | 2 +- demo/file_explorer_component_events/run.py | 2 +- demo/logoutbutton_component/requirements.txt | 1 - demo/logoutbutton_component/run.ipynb | 1 - demo/logoutbutton_component/run.py | 6 - gradio/__init__.py | 2 - gradio/blocks.py | 9 +- gradio/chat_interface.py | 9 - gradio/cli/commands/components/show.py | 5 +- gradio/components/__init__.py | 1 - gradio/components/file_explorer.py | 8 - gradio/components/login_button.py | 5 - gradio/components/logout_button.py | 67 ----- gradio/events.py | 41 +-- gradio/flagging.py | 276 ------------------ gradio/interface.py | 8 +- guides/09_other-tutorials/using-flagging.md | 44 --- test/components/test_file_explorer.py | 2 +- test/conftest.py | 1 - test/test_blocks.py | 13 +- test/test_buttons.py | 6 - test/test_flagging.py | 80 +---- test/test_routes.py | 2 +- 26 files changed, 22 insertions(+), 617 deletions(-) create mode 100644 .changeset/rotten-bears-bathe.md delete mode 100644 demo/logoutbutton_component/requirements.txt delete mode 100644 demo/logoutbutton_component/run.ipynb delete mode 100644 demo/logoutbutton_component/run.py delete mode 100644 gradio/components/logout_button.py diff --git a/.changeset/rotten-bears-bathe.md b/.changeset/rotten-bears-bathe.md new file mode 100644 index 0000000000000..248148d7a457f --- /dev/null +++ b/.changeset/rotten-bears-bathe.md @@ -0,0 +1,5 @@ +--- +"gradio": major +--- + +feat:Deprecate for 5.0 diff --git a/client/python/test/conftest.py b/client/python/test/conftest.py index a46ea99c54857..4e063c57d301c 100644 --- a/client/python/test/conftest.py +++ b/client/python/test/conftest.py @@ -238,32 +238,6 @@ def show(n): return demo -@pytest.fixture -def count_generator_demo_exception(): - def count(n): - for i in range(int(n)): - time.sleep(0.01) - if i == 5: - raise ValueError("Oh no!") - yield i - - def show(n): - return str(list(range(int(n)))) - - with gr.Blocks() as demo: - with gr.Column(): - num = gr.Number(value=10) - with gr.Row(): - count_btn = gr.Button("Count") - count_forever = gr.Button("Count forever") - with gr.Column(): - out = gr.Textbox() - - count_btn.click(count, num, out, api_name="count") - count_forever.click(show, num, out, api_name="count_forever", every=3) - return demo - - @pytest.fixture def file_io_demo(): demo = gr.Interface( diff --git a/client/python/test/test_client.py b/client/python/test/test_client.py index 240f0018bf62b..8037117812576 100644 --- a/client/python/test/test_client.py +++ b/client/python/test/test_client.py @@ -22,7 +22,7 @@ from gradio_client import Client, handle_file from gradio_client.client import DEFAULT_TEMP_DIR -from gradio_client.exceptions import AppError, AuthenticationError +from gradio_client.exceptions import AuthenticationError from gradio_client.utils import ( Communicator, ProgressUnit, @@ -1391,18 +1391,3 @@ def test_add_secrets(self, mock_time, mock_init, mock_duplicate, mock_add_secret "test_value2", token=HF_TOKEN, ) - - -def test_upstream_exceptions(count_generator_demo_exception): - with connect(count_generator_demo_exception, show_error=True) as client: - with pytest.raises( - AppError, match="The upstream Gradio app has raised an exception: Oh no!" - ): - client.predict(7, api_name="/count") - - with connect(count_generator_demo_exception) as client: - with pytest.raises( - AppError, - match="The upstream Gradio app has raised an exception but has not enabled verbose error reporting.", - ): - client.predict(7, api_name="/count") diff --git a/demo/file_explorer_component_events/run.ipynb b/demo/file_explorer_component_events/run.ipynb index 8dc663c498903..a864def2c9cc2 100644 --- a/demo/file_explorer_component_events/run.ipynb +++ b/demo/file_explorer_component_events/run.ipynb @@ -1 +1 @@ -{"cells": [{"cell_type": "markdown", "id": "302934307671667531413257853548643485645", "metadata": {}, "source": ["# Gradio Demo: file_explorer_component_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": ["# Downloading files from the demo repo\n", "import os\n", "os.mkdir('dir1')\n", "!wget -q -O dir1/bar.txt https://github.com/gradio-app/gradio/raw/main/demo/file_explorer_component_events/dir1/bar.txt\n", "!wget -q -O dir1/foo.txt https://github.com/gradio-app/gradio/raw/main/demo/file_explorer_component_events/dir1/foo.txt\n", "os.mkdir('dir2')\n", "!wget -q -O dir2/baz.png https://github.com/gradio-app/gradio/raw/main/demo/file_explorer_component_events/dir2/baz.png\n", "!wget -q -O dir2/foo.png https://github.com/gradio-app/gradio/raw/main/demo/file_explorer_component_events/dir2/foo.png\n", "os.mkdir('dir3')\n", "!wget -q -O dir3/dir3_bar.log https://github.com/gradio-app/gradio/raw/main/demo/file_explorer_component_events/dir3/dir3_bar.log\n", "!wget -q -O dir3/dir3_foo.txt https://github.com/gradio-app/gradio/raw/main/demo/file_explorer_component_events/dir3/dir3_foo.txt\n", "!wget -q -O dir3/dir4 https://github.com/gradio-app/gradio/raw/main/demo/file_explorer_component_events/dir3/dir4"]}, {"cell_type": "code", "execution_count": null, "id": "44380577570523278879349135829904343037", "metadata": {}, "outputs": [], "source": ["import gradio as gr\n", "from pathlib import Path\n", "\n", "base_root = Path(__file__).parent.resolve()\n", "\n", "with gr.Blocks() as demo:\n", " with gr.Row():\n", " dd = gr.Dropdown(label=\"Select File Explorer Root\",\n", " value=str(base_root / \"dir1\"),\n", " choices=[str(base_root / \"dir1\"), str(base_root / \"dir2\"),\n", " str(base_root / \"dir3\")])\n", " with gr.Group():\n", " txt_only_glob = gr.Checkbox(label=\"Show only text files\", value=False)\n", " ignore_txt_in_glob = gr.Checkbox(label=\"Ignore text files in glob\", value=False)\n", "\n", " fe = gr.FileExplorer(root_dir=str(base_root / \"dir1\"),\n", " glob=\"**/*\", interactive=True)\n", " textbox = gr.Textbox(label=\"Selected Directory\")\n", " run = gr.Button(\"Run\")\n", " total_changes = gr.Number(0, elem_id=\"total-changes\")\n", " \n", " txt_only_glob.select(lambda s: gr.FileExplorer(glob=\"*.txt\" if s else \"*\") ,\n", " inputs=[txt_only_glob], outputs=[fe])\n", " ignore_txt_in_glob.select(lambda s: gr.FileExplorer(ignore_glob=\"*.txt\" if s else None),\n", " inputs=[ignore_txt_in_glob], outputs=[fe]) \n", "\n", " dd.select(lambda s: gr.FileExplorer(root=s), inputs=[dd], outputs=[fe])\n", " run.click(lambda s: \",\".join(s) if isinstance(s, list) else s, inputs=[fe], outputs=[textbox])\n", " fe.change(lambda num: num + 1, inputs=total_changes, outputs=total_changes)\n", "\n", " with gr.Row():\n", " a = gr.Textbox(elem_id=\"input-box\")\n", " a.change(lambda x: x, inputs=[a])\n", "\n", "\n", "if __name__ == \"__main__\":\n", " demo.launch()"]}], "metadata": {}, "nbformat": 4, "nbformat_minor": 5} \ No newline at end of file +{"cells": [{"cell_type": "markdown", "id": "302934307671667531413257853548643485645", "metadata": {}, "source": ["# Gradio Demo: file_explorer_component_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": ["# Downloading files from the demo repo\n", "import os\n", "os.mkdir('dir1')\n", "!wget -q -O dir1/bar.txt https://github.com/gradio-app/gradio/raw/main/demo/file_explorer_component_events/dir1/bar.txt\n", "!wget -q -O dir1/foo.txt https://github.com/gradio-app/gradio/raw/main/demo/file_explorer_component_events/dir1/foo.txt\n", "os.mkdir('dir2')\n", "!wget -q -O dir2/baz.png https://github.com/gradio-app/gradio/raw/main/demo/file_explorer_component_events/dir2/baz.png\n", "!wget -q -O dir2/foo.png https://github.com/gradio-app/gradio/raw/main/demo/file_explorer_component_events/dir2/foo.png\n", "os.mkdir('dir3')\n", "!wget -q -O dir3/dir3_bar.log https://github.com/gradio-app/gradio/raw/main/demo/file_explorer_component_events/dir3/dir3_bar.log\n", "!wget -q -O dir3/dir3_foo.txt https://github.com/gradio-app/gradio/raw/main/demo/file_explorer_component_events/dir3/dir3_foo.txt\n", "!wget -q -O dir3/dir4 https://github.com/gradio-app/gradio/raw/main/demo/file_explorer_component_events/dir3/dir4"]}, {"cell_type": "code", "execution_count": null, "id": "44380577570523278879349135829904343037", "metadata": {}, "outputs": [], "source": ["import gradio as gr\n", "from pathlib import Path\n", "\n", "base_root = Path(__file__).parent.resolve()\n", "\n", "with gr.Blocks() as demo:\n", " with gr.Row():\n", " dd = gr.Dropdown(label=\"Select File Explorer Root\",\n", " value=str(base_root / \"dir1\"),\n", " choices=[str(base_root / \"dir1\"), str(base_root / \"dir2\"),\n", " str(base_root / \"dir3\")])\n", " with gr.Group():\n", " txt_only_glob = gr.Checkbox(label=\"Show only text files\", value=False)\n", " ignore_txt_in_glob = gr.Checkbox(label=\"Ignore text files in glob\", value=False)\n", "\n", " fe = gr.FileExplorer(root_dir=str(base_root / \"dir1\"),\n", " glob=\"**/*\", interactive=True)\n", " textbox = gr.Textbox(label=\"Selected Directory\")\n", " run = gr.Button(\"Run\")\n", " total_changes = gr.Number(0, elem_id=\"total-changes\")\n", " \n", " txt_only_glob.select(lambda s: gr.FileExplorer(glob=\"*.txt\" if s else \"*\") ,\n", " inputs=[txt_only_glob], outputs=[fe])\n", " ignore_txt_in_glob.select(lambda s: gr.FileExplorer(ignore_glob=\"*.txt\" if s else None),\n", " inputs=[ignore_txt_in_glob], outputs=[fe]) \n", "\n", " dd.select(lambda s: gr.FileExplorer(root_dir=s), inputs=[dd], outputs=[fe])\n", " run.click(lambda s: \",\".join(s) if isinstance(s, list) else s, inputs=[fe], outputs=[textbox])\n", " fe.change(lambda num: num + 1, inputs=total_changes, outputs=total_changes)\n", "\n", " with gr.Row():\n", " a = gr.Textbox(elem_id=\"input-box\")\n", " a.change(lambda x: x, inputs=[a])\n", "\n", "\n", "if __name__ == \"__main__\":\n", " demo.launch()"]}], "metadata": {}, "nbformat": 4, "nbformat_minor": 5} \ No newline at end of file diff --git a/demo/file_explorer_component_events/run.py b/demo/file_explorer_component_events/run.py index c991bc9e55f6c..fd811688ec662 100644 --- a/demo/file_explorer_component_events/run.py +++ b/demo/file_explorer_component_events/run.py @@ -24,7 +24,7 @@ ignore_txt_in_glob.select(lambda s: gr.FileExplorer(ignore_glob="*.txt" if s else None), inputs=[ignore_txt_in_glob], outputs=[fe]) - dd.select(lambda s: gr.FileExplorer(root=s), inputs=[dd], outputs=[fe]) + dd.select(lambda s: gr.FileExplorer(root_dir=s), inputs=[dd], outputs=[fe]) run.click(lambda s: ",".join(s) if isinstance(s, list) else s, inputs=[fe], outputs=[textbox]) fe.change(lambda num: num + 1, inputs=total_changes, outputs=total_changes) diff --git a/demo/logoutbutton_component/requirements.txt b/demo/logoutbutton_component/requirements.txt deleted file mode 100644 index f7359a07d4b7d..0000000000000 --- a/demo/logoutbutton_component/requirements.txt +++ /dev/null @@ -1 +0,0 @@ -gradio[oauth] \ No newline at end of file diff --git a/demo/logoutbutton_component/run.ipynb b/demo/logoutbutton_component/run.ipynb deleted file mode 100644 index 954963165387a..0000000000000 --- a/demo/logoutbutton_component/run.ipynb +++ /dev/null @@ -1 +0,0 @@ -{"cells": [{"cell_type": "markdown", "id": "302934307671667531413257853548643485645", "metadata": {}, "source": ["# Gradio Demo: logoutbutton_component"]}, {"cell_type": "code", "execution_count": null, "id": "272996653310673477252411125948039410165", "metadata": {}, "outputs": [], "source": ["!pip install -q gradio gradio[oauth]"]}, {"cell_type": "code", "execution_count": null, "id": "288918539441861185822528903084949547379", "metadata": {}, "outputs": [], "source": ["import gradio as gr \n", "\n", "with gr.Blocks() as demo:\n", " gr.LogoutButton()\n", "\n", "demo.launch()"]}], "metadata": {}, "nbformat": 4, "nbformat_minor": 5} \ No newline at end of file diff --git a/demo/logoutbutton_component/run.py b/demo/logoutbutton_component/run.py deleted file mode 100644 index 05e04c6e8d361..0000000000000 --- a/demo/logoutbutton_component/run.py +++ /dev/null @@ -1,6 +0,0 @@ -import gradio as gr - -with gr.Blocks() as demo: - gr.LogoutButton() - -demo.launch() \ No newline at end of file diff --git a/gradio/__init__.py b/gradio/__init__.py index 008c63bd4cfb5..e5abf7924d147 100644 --- a/gradio/__init__.py +++ b/gradio/__init__.py @@ -42,7 +42,6 @@ Label, LinePlot, LoginButton, - LogoutButton, Markdown, MessageDict, Model3D, @@ -77,7 +76,6 @@ from gradio.flagging import ( CSVLogger, FlaggingCallback, - HuggingFaceDatasetSaver, SimpleCSVLogger, ) from gradio.helpers import ( diff --git a/gradio/blocks.py b/gradio/blocks.py index d2b771c747b29..985d328f012c3 100644 --- a/gradio/blocks.py +++ b/gradio/blocks.py @@ -1266,8 +1266,7 @@ def __repr__(self): def expects_oauth(self): """Return whether the app expects user to authenticate via OAuth.""" return any( - isinstance(block, (components.LoginButton, components.LogoutButton)) - for block in self.blocks.values() + isinstance(block, components.LoginButton) for block in self.blocks.values() ) def unload(self, fn: Callable): @@ -2064,7 +2063,6 @@ def queue( status_update_rate: float | Literal["auto"] = "auto", api_open: bool | None = None, max_size: int | None = None, - concurrency_count: int | None = None, *, default_concurrency_limit: int | None | Literal["not_set"] = "not_set", ): @@ -2074,7 +2072,6 @@ def queue( status_update_rate: If "auto", Queue will send status estimations to all clients whenever a job is finished. Otherwise Queue will send status at regular intervals set by this parameter as the number of seconds. api_open: If True, the REST routes of the backend will be open, allowing requests made directly to those endpoints to skip the queue. max_size: The maximum number of events the queue will store at any given moment. If the queue is full, new events will not be added and a user will receive a message saying that the queue is full. If None, the queue size will be unlimited. - concurrency_count: Deprecated. Set the concurrency_limit directly on event listeners e.g. btn.click(fn, ..., concurrency_limit=10) or gr.Interface(concurrency_limit=10). If necessary, the total number of workers can be configured via `max_threads` in launch(). default_concurrency_limit: The default value of `concurrency_limit` to use for event listeners that don't specify a value. Can be set by environment variable GRADIO_DEFAULT_CONCURRENCY_LIMIT. Defaults to 1 if not set otherwise. Example: (Blocks) with gr.Blocks() as demo: @@ -2087,10 +2084,6 @@ def queue( demo.queue(max_size=20) demo.launch() """ - if concurrency_count: - raise DeprecationWarning( - "concurrency_count has been deprecated. Set the concurrency_limit directly on event listeners e.g. btn.click(fn, ..., concurrency_limit=10) or gr.Interface(concurrency_limit=10). If necessary, the total number of workers can be configured via `max_threads` in launch()." - ) if api_open is not None: self.api_open = api_open if utils.is_zero_gpu_space(): diff --git a/gradio/chat_interface.py b/gradio/chat_interface.py index 3e174004fd620..42b121000e829 100644 --- a/gradio/chat_interface.py +++ b/gradio/chat_interface.py @@ -64,7 +64,6 @@ def __init__( chatbot: Chatbot | None = None, textbox: Textbox | MultimodalTextbox | None = None, additional_inputs: str | Component | list[str | Component] | None = None, - additional_inputs_accordion_name: str | None = None, additional_inputs_accordion: str | Accordion | None = None, examples: list[str] | list[dict[str, str | list]] | list[list] | None = None, cache_examples: bool | Literal["lazy"] | None = None, @@ -94,7 +93,6 @@ def __init__( chatbot: An instance of the gr.Chatbot component to use for the chat interface, if you would like to customize the chatbot properties. If not provided, a default gr.Chatbot component will be created. textbox: An instance of the gr.Textbox or gr.MultimodalTextbox component to use for the chat interface, if you would like to customize the textbox properties. If not provided, a default gr.Textbox or gr.MultimodalTextbox component will be created. additional_inputs: An instance or list of instances of gradio components (or their string shortcuts) to use as additional inputs to the chatbot. If components are not already rendered in a surrounding Blocks, then the components will be displayed under the chatbot, in an accordion. - additional_inputs_accordion_name: Deprecated. Will be removed in a future version of Gradio. Use the `additional_inputs_accordion` parameter instead. additional_inputs_accordion: If a string is provided, this is the label of the `gr.Accordion` to use to contain additional inputs. A `gr.Accordion` object can be provided as well to configure other properties of the container holding the additional inputs. Defaults to a `gr.Accordion(label="Additional Inputs", open=False)`. This parameter is only used if `additional_inputs` is provided. examples: Sample inputs for the function; if provided, appear below the chatbot and can be clicked to populate the chatbot input. Should be a list of strings if `multimodal` is False, and a list of dictionaries (with keys `text` and `files`) if `multimodal` is True. cache_examples: If True, caches examples in the server for fast runtime in examples. The default option in HuggingFace Spaces is True. The default option elsewhere is False. @@ -152,13 +150,6 @@ def __init__( ] else: self.additional_inputs = [] - if additional_inputs_accordion_name is not None: - print( - "The `additional_inputs_accordion_name` parameter is deprecated and will be removed in a future version of Gradio. Use the `additional_inputs_accordion` parameter instead." - ) - self.additional_inputs_accordion_params = { - "label": additional_inputs_accordion_name - } if additional_inputs_accordion is None: self.additional_inputs_accordion_params = { "label": "Additional Inputs", diff --git a/gradio/cli/commands/components/show.py b/gradio/cli/commands/components/show.py index b9d8688ac908f..5ab8bcfd7421e 100644 --- a/gradio/cli/commands/components/show.py +++ b/gradio/cli/commands/components/show.py @@ -26,6 +26,7 @@ "FormComponent", "Fallback", "State", + "LogoutButton", } _BEGINNER_FRIENDLY = {"Slider", "Radio", "Checkbox", "Number", "CheckboxGroup", "File"} @@ -34,10 +35,12 @@ def _get_table_items(module): items = [] for name in module.__all__: + if name in _IGNORE: + continue gr_cls = getattr(module, name) if not ( inspect.isclass(gr_cls) and issubclass(gr_cls, (Component, BlockContext)) - ) or (name in _IGNORE): + ): continue tags = [] if "Simple" in name or name in _BEGINNER_FRIENDLY: diff --git a/gradio/components/__init__.py b/gradio/components/__init__.py index 93172601a085a..b7536334b3e50 100644 --- a/gradio/components/__init__.py +++ b/gradio/components/__init__.py @@ -35,7 +35,6 @@ from gradio.components.label import Label from gradio.components.line_plot import LinePlot from gradio.components.login_button import LoginButton -from gradio.components.logout_button import LogoutButton from gradio.components.markdown import Markdown from gradio.components.model3d import Model3D from gradio.components.multimodal_textbox import MultimodalTextbox diff --git a/gradio/components/file_explorer.py b/gradio/components/file_explorer.py index 6fa5bedfae0b2..80c454b52a793 100644 --- a/gradio/components/file_explorer.py +++ b/gradio/components/file_explorer.py @@ -4,7 +4,6 @@ import fnmatch import os -import warnings from pathlib import Path from typing import TYPE_CHECKING, Any, Callable, List, Literal @@ -55,7 +54,6 @@ def __init__( elem_classes: list[str] | str | None = None, render: bool = True, key: int | str | None = None, - root: None = None, ): """ Parameters: @@ -79,12 +77,6 @@ def __init__( render: If False, component will not render be rendered in the Blocks context. Should be used if the intention is to assign event listeners now but render the component later. key: if assigned, will be used to assume identity across a re-render. Components that have the same key across a re-render will have their value preserved. """ - if root is not None: - warnings.warn( - "The `root` parameter has been deprecated. Please use `root_dir` instead." - ) - root_dir = root - self._constructor_args[0]["root_dir"] = root self.root_dir = os.path.abspath(root_dir) self.glob = glob self.ignore_glob = ignore_glob diff --git a/gradio/components/login_button.py b/gradio/components/login_button.py index cb8f5a7d379dd..bb3cd454cdceb 100644 --- a/gradio/components/login_button.py +++ b/gradio/components/login_button.py @@ -45,16 +45,11 @@ def __init__( key: int | str | None = None, scale: int | None = 0, min_width: int | None = None, - signed_in_value: str = "Signed in as {}", ): """ Parameters: logout_value: The text to display when the user is signed in. The string should contain a placeholder for the username with a call-to-action to logout, e.g. "Logout ({})". """ - if signed_in_value != "Signed in as {}": - warnings.warn( - "The `signed_in_value` parameter is deprecated. Please use `logout_value` instead." - ) self.logout_value = logout_value super().__init__( value, diff --git a/gradio/components/logout_button.py b/gradio/components/logout_button.py deleted file mode 100644 index 9655f8fffa604..0000000000000 --- a/gradio/components/logout_button.py +++ /dev/null @@ -1,67 +0,0 @@ -"""Predefined button to sign out from Hugging Face in a Gradio Space.""" - -from __future__ import annotations - -import warnings -from typing import TYPE_CHECKING, Literal - -from gradio_client.documentation import document - -from gradio.components import Button, Component - -if TYPE_CHECKING: - from gradio.components import Timer - - -@document() -class LogoutButton(Button): - """ - Creates a Button to log out a user from a Space using OAuth. - - Note: `LogoutButton` component is deprecated. Please use `gr.LoginButton` instead - which handles both the login and logout processes. - """ - - is_template = True - - def __init__( - self, - value: str = "Logout", - *, - every: Timer | float | None = None, - inputs: Component | list[Component] | set[Component] | None = None, - variant: Literal["primary", "secondary", "stop"] = "secondary", - size: Literal["sm", "lg"] | None = None, - icon: str - | None = "https://huggingface.co/front/assets/huggingface_logo-noborder.svg", - # Link to logout page (which will delete the session cookie and redirect to landing page). - link: str | None = "/logout", - visible: bool = True, - interactive: bool = True, - elem_id: str | None = None, - elem_classes: list[str] | str | None = None, - render: bool = True, - key: int | str | None = None, - scale: int | None = 0, - min_width: int | None = None, - ): - warnings.warn( - "The `gr.LogoutButton` component is deprecated. Please use `gr.LoginButton` instead which handles both the login and logout processes." - ) - super().__init__( - value, - every=every, - inputs=inputs, - variant=variant, - size=size, - icon=icon, - link=link, - visible=visible, - interactive=interactive, - elem_id=elem_id, - elem_classes=elem_classes, - render=render, - key=key, - scale=scale, - min_width=min_width, - ) diff --git a/gradio/events.py b/gradio/events.py index 90dbe2a7347d9..20caf4d8f6cb7 100644 --- a/gradio/events.py +++ b/gradio/events.py @@ -408,7 +408,6 @@ def event_trigger( preprocess: bool = True, postprocess: bool = True, cancels: dict[str, Any] | list[dict[str, Any]] | None = None, - every: float | None = None, trigger_mode: Literal["once", "multiple", "always_last"] | None = None, js: str | None = None, concurrency_limit: int | None | Literal["default"] = "default", @@ -429,7 +428,6 @@ def event_trigger( preprocess: If False, will not run preprocessing of component data before running 'fn' (e.g. leaving it as a base64 string if this method is called with the `Image` component). postprocess: If False, will not run postprocessing of component data before returning 'fn' output to the browser. cancels: A list of other events to cancel when this listener is triggered. For example, setting cancels=[click_event] will cancel the click_event, where click_event is the return value of another components .click method. Functions that have not yet run (or generators that are iterating) will be cancelled, but functions that are currently running will be allowed to finish. - every: Will be deprecated in favor of gr.Timer. Run this event 'every' number of seconds while the client connection is open. Interpreted in seconds. trigger_mode: If "once" (default for all events except `.change()`) would not allow any submissions while an event is pending. If set to "multiple", unlimited submissions are allowed while pending, and "always_last" (default for `.change()` and `.key_up()` events) would allow a second submission after the pending event is complete. 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. concurrency_limit: If set, this is the maximum number of this event that can be running simultaneously. Can be set to None to mean no concurrency_limit (any number of this event can be running simultaneously). Set to "default" to use the default concurrency limit (defined by the `default_concurrency_limit` parameter in `Blocks.queue()`, which itself is 1 by default). @@ -486,32 +484,15 @@ def inner(*args, **kwargs): block if _has_trigger else None, _event_name ) - # Handle every as a float (to be deprecated in favor of gr.Timer) - timer = None - if every is not None: - from gradio.components import Timer - - timer = Timer(every, active=False) - root_block.set_event_trigger( - [event_target], - lambda: Timer(active=True), - None, - timer, - show_api=False, - ) - target = EventListenerMethod(timer, "tick") - else: - target = event_target - dep, dep_index = root_block.set_event_trigger( - [target], + [event_target], fn, inputs, outputs, preprocess=preprocess, postprocess=postprocess, scroll_to_output=scroll_to_output, - show_progress=show_progress if every is None else "hidden", + show_progress=show_progress, api_name=api_name, js=js, concurrency_limit=concurrency_limit, @@ -530,7 +511,7 @@ def inner(*args, **kwargs): ) if _callback: _callback(block) - return Dependency(block, dep.get_config(), dep_index, fn, timer) + return Dependency(block, dep.get_config(), dep_index, fn) event_trigger.event_name = _event_name event_trigger.has_trigger = _has_trigger @@ -555,7 +536,6 @@ def on( postprocess: bool = True, cancels: dict[str, Any] | list[dict[str, Any]] | None = None, trigger_mode: Literal["once", "multiple", "always_last"] | None = None, - every: float | None = None, js: str | None = None, concurrency_limit: int | None | Literal["default"] = "default", concurrency_id: str | None = None, @@ -581,7 +561,6 @@ def on( postprocess: If False, will not run postprocessing of component data before returning 'fn' output to the browser. cancels: A list of other events to cancel when this listener is triggered. For example, setting cancels=[click_event] will cancel the click_event, where click_event is the return value of another components .click method. Functions that have not yet run (or generators that are iterating) will be cancelled, but functions that are currently running will be allowed to finish. trigger_mode: If "once" (default for all events except `.change()`) would not allow any submissions while an event is pending. If set to "multiple", unlimited submissions are allowed while pending, and "always_last" (default for `.change()` and `.key_up()` events) would allow a second submission after the pending event is complete. - every: Will be deprecated in favor of gr.Timer. Run this event 'every' number of seconds while the client connection is open. Interpreted in seconds. js: Optional frontend js method to run before running 'fn'. Input arguments for js method are values of 'inputs', return should be a list of values for output components. concurrency_limit: If set, this is the maximum number of this event that can be running simultaneously. Can be set to None to mean no concurrency_limit (any number of this event can be running simultaneously). Set to "default" to use the default concurrency limit (defined by the `default_concurrency_limit` parameter in `Blocks.queue()`, which itself is 1 by default). concurrency_id: If set, this is the id of the concurrency group. Events with the same concurrency_id will be limited by the lowest set concurrency_limit. @@ -632,7 +611,6 @@ def wrapper(func): concurrency_id=concurrency_id, show_api=show_api, trigger_mode=trigger_mode, - every=every, ) @wraps(func) @@ -662,19 +640,6 @@ def inner(*args, **kwargs): if trigger.callback: trigger.callback(trigger.__self__) - if every is not None: - from gradio.components import Timer - - timer = Timer(every, active=False) - root_block.set_event_trigger( - methods, - lambda: Timer(active=True), - None, - timer, - show_api=False, - ) - methods = [EventListenerMethod(timer, "tick")] - dep, dep_index = root_block.set_event_trigger( methods, fn, diff --git a/gradio/flagging.py b/gradio/flagging.py index 0bd164752bf42..027d34fb0e882 100644 --- a/gradio/flagging.py +++ b/gradio/flagging.py @@ -2,17 +2,12 @@ import csv import datetime -import json import os import time -import uuid from abc import ABC, abstractmethod -from collections import OrderedDict from pathlib import Path from typing import TYPE_CHECKING, Any -import filelock -import huggingface_hub from gradio_client import utils as client_utils from gradio_client.documentation import document @@ -190,277 +185,6 @@ def flag( return line_count -@document() -class HuggingFaceDatasetSaver(FlaggingCallback): - """ - A callback that saves each flagged sample (both the input and output data) to a HuggingFace dataset. - - Example: - import gradio as gr - hf_writer = gr.HuggingFaceDatasetSaver(HF_API_TOKEN, "image-classification-mistakes") - def image_classifier(inp): - return {'cat': 0.3, 'dog': 0.7} - demo = gr.Interface(fn=image_classifier, inputs="image", outputs="label", - allow_flagging="manual", flagging_callback=hf_writer) - Guides: using-flagging - """ - - def __init__( - self, - hf_token: str, - dataset_name: str, - private: bool = False, - info_filename: str = "dataset_info.json", - separate_dirs: bool = False, - ): - """ - Parameters: - hf_token: The HuggingFace token to use to create (and write the flagged sample to) the HuggingFace dataset (defaults to the registered one). - dataset_name: The repo_id of the dataset to save the data to, e.g. "image-classifier-1" or "username/image-classifier-1". - private: Whether the dataset should be private (defaults to False). - info_filename: The name of the file to save the dataset info (defaults to "dataset_infos.json"). - separate_dirs: If True, each flagged item will be saved in a separate directory. This makes the flagging more robust to concurrent editing, but may be less convenient to use. - """ - self.hf_token = hf_token - self.dataset_id = dataset_name # TODO: rename parameter (but ensure backward compatibility somehow) - self.dataset_private = private - self.info_filename = info_filename - self.separate_dirs = separate_dirs - - def setup(self, components: list[Component], flagging_dir: str): - """ - Params: - flagging_dir (str): local directory where the dataset is cloned, - updated, and pushed from. - """ - # Setup dataset on the Hub - self.dataset_id = huggingface_hub.create_repo( - repo_id=self.dataset_id, - token=self.hf_token, - private=self.dataset_private, - repo_type="dataset", - exist_ok=True, - ).repo_id - path_glob = "**/*.jsonl" if self.separate_dirs else "data.csv" - huggingface_hub.metadata_update( - repo_id=self.dataset_id, - repo_type="dataset", - metadata={ - "configs": [ - { - "config_name": "default", - "data_files": [{"split": "train", "path": path_glob}], - } - ] - }, - overwrite=True, - token=self.hf_token, - ) - - # Setup flagging dir - self.components = components - self.dataset_dir = ( - Path(flagging_dir).absolute() / self.dataset_id.split("/")[-1] - ) - self.dataset_dir.mkdir(parents=True, exist_ok=True) - self.infos_file = self.dataset_dir / self.info_filename - - # Download remote files to local - remote_files = [self.info_filename] - if not self.separate_dirs: - # No separate dirs => means all data is in the same CSV file => download it to get its current content - remote_files.append("data.csv") - - for filename in remote_files: - try: - huggingface_hub.hf_hub_download( - repo_id=self.dataset_id, - repo_type="dataset", - filename=filename, - local_dir=self.dataset_dir, - token=self.hf_token, - ) - except huggingface_hub.utils.EntryNotFoundError: - pass - - def flag( - self, - flag_data: list[Any], - flag_option: str = "", - username: str | None = None, - ) -> int: - if self.separate_dirs: - # JSONL files to support dataset preview on the Hub - unique_id = str(uuid.uuid4()) - components_dir = self.dataset_dir / unique_id - data_file = components_dir / "metadata.jsonl" - path_in_repo = unique_id # upload in sub folder (safer for concurrency) - else: - # Unique CSV file - components_dir = self.dataset_dir - data_file = components_dir / "data.csv" - path_in_repo = None # upload at root level - - return self._flag_in_dir( - data_file=data_file, - components_dir=components_dir, - path_in_repo=path_in_repo, - flag_data=flag_data, - flag_option=flag_option, - username=username or "", - ) - - def _flag_in_dir( - self, - data_file: Path, - components_dir: Path, - path_in_repo: str | None, - flag_data: list[Any], - flag_option: str = "", - username: str = "", - ) -> int: - # Deserialize components (write images/audio to files) - features, row = self._deserialize_components( - components_dir, flag_data, flag_option, username - ) - - # Write generic info to dataset_infos.json + upload - with filelock.FileLock(str(self.infos_file) + ".lock"): - if not self.infos_file.exists(): - self.infos_file.write_text( - json.dumps({"flagged": {"features": features}}) - ) - - huggingface_hub.upload_file( - repo_id=self.dataset_id, - repo_type="dataset", - token=self.hf_token, - path_in_repo=self.infos_file.name, - path_or_fileobj=self.infos_file, - ) - - headers = list(features.keys()) - - if not self.separate_dirs: - with filelock.FileLock(components_dir / ".lock"): - sample_nb = self._save_as_csv(data_file, headers=headers, row=row) - sample_name = str(sample_nb) - huggingface_hub.upload_folder( - repo_id=self.dataset_id, - repo_type="dataset", - commit_message=f"Flagged sample #{sample_name}", - path_in_repo=path_in_repo, - ignore_patterns="*.lock", - folder_path=components_dir, - token=self.hf_token, - ) - else: - sample_name = self._save_as_jsonl(data_file, headers=headers, row=row) - sample_nb = len( - [path for path in self.dataset_dir.iterdir() if path.is_dir()] - ) - huggingface_hub.upload_folder( - repo_id=self.dataset_id, - repo_type="dataset", - commit_message=f"Flagged sample #{sample_name}", - path_in_repo=path_in_repo, - ignore_patterns="*.lock", - folder_path=components_dir, - token=self.hf_token, - ) - - return sample_nb - - @staticmethod - def _save_as_csv(data_file: Path, headers: list[str], row: list[Any]) -> int: - """Save data as CSV and return the sample name (row number).""" - is_new = not data_file.exists() - - with data_file.open("a", newline="", encoding="utf-8") as csvfile: - writer = csv.writer(csvfile) - - # Write CSV headers if new file - if is_new: - writer.writerow(utils.sanitize_list_for_csv(headers)) - - # Write CSV row for flagged sample - writer.writerow(utils.sanitize_list_for_csv(row)) - - with data_file.open(encoding="utf-8") as csvfile: - return sum(1 for _ in csv.reader(csvfile)) - 1 - - @staticmethod - def _save_as_jsonl(data_file: Path, headers: list[str], row: list[Any]) -> str: - """Save data as JSONL and return the sample name (uuid).""" - Path.mkdir(data_file.parent, parents=True, exist_ok=True) - with open(data_file, "w", encoding="utf-8") as f: - json.dump(dict(zip(headers, row)), f) - return data_file.parent.name - - def _deserialize_components( - self, - data_dir: Path, - flag_data: list[Any], - flag_option: str = "", - username: str = "", - ) -> tuple[dict[Any, Any], list[Any]]: - """Deserialize components and return the corresponding row for the flagged sample. - - Images/audio are saved to disk as individual files. - """ - # Components that can have a preview on dataset repos - file_preview_types = {gr.Audio: "Audio", gr.Image: "Image"} - - # Generate the row corresponding to the flagged sample - features = OrderedDict() - row = [] - for component, sample in zip(self.components, flag_data): - # Get deserialized object (will save sample to disk if applicable -file, audio, image,...-) - label = component.label or "" - save_dir = data_dir / client_utils.strip_invalid_filename_characters(label) - save_dir.mkdir(exist_ok=True, parents=True) - deserialized = utils.simplify_file_data_in_str( - component.flag(sample, save_dir) - ) - - # Add deserialized object to row - features[label] = {"dtype": "string", "_type": "Value"} - try: - deserialized_path = Path(deserialized) - if not deserialized_path.exists(): - raise FileNotFoundError(f"File {deserialized} not found") - row.append(str(deserialized_path.relative_to(self.dataset_dir))) - except (FileNotFoundError, TypeError, ValueError): - deserialized = "" if deserialized is None else str(deserialized) - row.append(deserialized) - - # If component is eligible for a preview, add the URL of the file - # Be mindful that images and audio can be None - if isinstance(component, tuple(file_preview_types)): # type: ignore - for _component, _type in file_preview_types.items(): - if isinstance(component, _component): - features[label + " file"] = {"_type": _type} - break - if deserialized: - path_in_repo = str( # returned filepath is absolute, we want it relative to compute URL - Path(deserialized).relative_to(self.dataset_dir) - ).replace("\\", "/") - row.append( - huggingface_hub.hf_hub_url( - repo_id=self.dataset_id, - filename=path_in_repo, - repo_type="dataset", - ) - ) - else: - row.append("") - features["flag"] = {"dtype": "string", "_type": "Value"} - features["username"] = {"dtype": "string", "_type": "Value"} - row.append(flag_option) - row.append(username) - return features, row - - class FlagMethod: """ Helper class that contains the flagging options and calls the flagging method. Also diff --git a/gradio/interface.py b/gradio/interface.py index d8963fd9645c2..8b6ec54193365 100644 --- a/gradio/interface.py +++ b/gradio/interface.py @@ -99,13 +99,14 @@ def __init__( inputs: str | Component | list[str | Component] | None, outputs: str | Component | list[str | Component] | None, examples: list[Any] | list[list[Any]] | str | None = None, + *, cache_examples: bool | Literal["lazy"] | None = None, examples_per_page: int = 10, + example_labels: list[str] | None = None, live: bool = False, title: str | None = None, description: str | None = None, article: str | None = None, - thumbnail: str | None = None, theme: Theme | str | None = None, css: str | None = None, allow_flagging: Literal["never"] @@ -126,13 +127,11 @@ def __init__( head: str | None = None, additional_inputs: str | Component | list[str | Component] | None = None, additional_inputs_accordion: str | Accordion | None = None, - *, submit_btn: str | Button = "Submit", stop_btn: str | Button = "Stop", clear_btn: str | Button | None = "Clear", delete_cache: tuple[int, int] | None = None, show_progress: Literal["full", "minimal", "hidden"] = "full", - example_labels: list[str] | None = None, **kwargs, ): """ @@ -147,7 +146,6 @@ def __init__( title: A title for the interface; if provided, appears above the input and output components in large font. Also used as the tab title when opened in a browser window. description: A description for the interface; if provided, appears above the input and output components and beneath the title in regular font. Accepts Markdown and HTML content. article: An expanded article explaining the interface; if provided, appears below the input and output components in regular font. Accepts Markdown and HTML content. If it is an HTTP(S) link to a downloadable remote file, the content of this file is displayed. - thumbnail: This parameter has been deprecated and has no effect. theme: A Theme object or a string representing a theme. If a string, will look for a built-in theme with that name (e.g. "soft" or "default"), or will attempt to load a theme from the Hugging Face Hub (e.g. "gradio/monochrome"). If None, will use the Default theme. css: Custom css as a string or path to a css file. This css will be included in the demo webpage. allow_flagging: One of "never", "auto", or "manual". If "never" or "auto", users will not see a button to flag an input and output. If "manual", users will see a button to flag. If "auto", every input the user submits will be automatically flagged, along with the generated output. If "manual", both the input and outputs are flagged when the user clicks flag button. This parameter can be set with environmental variable GRADIO_ALLOW_FLAGGING; otherwise defaults to "manual". @@ -318,8 +316,6 @@ def __init__( article = utils.download_if_url(article) self.article = article - self.thumbnail = thumbnail - self.examples = examples self.examples_per_page = examples_per_page self.example_labels = example_labels diff --git a/guides/09_other-tutorials/using-flagging.md b/guides/09_other-tutorials/using-flagging.md index d97b4533ba2bd..9682cfa416f97 100644 --- a/guides/09_other-tutorials/using-flagging.md +++ b/guides/09_other-tutorials/using-flagging.md @@ -28,7 +28,6 @@ There are [four parameters](https://gradio.app/docs/interface#initialization) in - `flagging_callback`: this parameter takes an instance of a subclass of the `FlaggingCallback` class - Using this parameter allows you to write custom code that gets run when the flag button is clicked - By default, this is set to an instance of `gr.CSVLogger` - - One example is setting it to an instance of `gr.HuggingFaceDatasetSaver` which can allow you to pipe any flagged data into a HuggingFace Dataset. (See more below.) ## What happens to flagged data? @@ -127,49 +126,6 @@ num1,operation,num2,Output,flag,timestamp 6,subtract,1.5,3.5,off by one,2022-02-04 11:42:32.062512 ``` -## The HuggingFaceDatasetSaver Callback - -Sometimes, saving the data to a local CSV file doesn't make sense. For example, on Hugging Face -Spaces, developers typically don't have access to the underlying ephemeral machine hosting the Gradio -demo. That's why, by default, flagging is turned off in Hugging Face Space. However, -you may want to do something else with the flagged data. - -We've made this super easy with the `flagging_callback` parameter. - -For example, below we're going to pipe flagged data from our calculator example into a Hugging Face Dataset, e.g. so that we can build a "crowd-sourced" dataset: - -```python -import os - -HF_TOKEN = os.getenv('HF_TOKEN') -hf_writer = gr.HuggingFaceDatasetSaver(HF_TOKEN, "crowdsourced-calculator-demo") - -iface = gr.Interface( - calculator, - ["number", gr.Radio(["add", "subtract", "multiply", "divide"]), "number"], - "number", - description="Check out the crowd-sourced dataset at: [https://huggingface.co/datasets/aliabd/crowdsourced-calculator-demo](https://huggingface.co/datasets/aliabd/crowdsourced-calculator-demo)", - allow_flagging="manual", - flagging_options=["wrong sign", "off by one", "other"], - flagging_callback=hf_writer -) - -iface.launch() -``` - -Notice that we define our own -instance of `gradio.HuggingFaceDatasetSaver` using our Hugging Face token and -the name of a dataset we'd like to save samples to. In addition, we also set `allow_flagging="manual"` -because on Hugging Face Spaces, `allow_flagging` is set to `"never"` by default. Here's our demo: - - - -You can now see all the examples flagged above in this [public Hugging Face dataset](https://huggingface.co/datasets/aliabd/crowdsourced-calculator-demo). - -![flagging callback hf](https://github.com/gradio-app/gradio/blob/main/guides/assets/flagging-callback-hf.png?raw=true) - -We created the `gradio.HuggingFaceDatasetSaver` class, but you can pass your own custom class as long as it inherits from `FLaggingCallback` defined in [this file](https://github.com/gradio-app/gradio/blob/master/gradio/flagging.py). If you create a cool callback, contribute it to the repo! - ## Flagging with Blocks What about if you are using `gradio.Blocks`? On one hand, you have even more flexibility diff --git a/test/components/test_file_explorer.py b/test/components/test_file_explorer.py index 0fcae37d6c93c..bc9371f7e0eea 100644 --- a/test/components/test_file_explorer.py +++ b/test/components/test_file_explorer.py @@ -52,7 +52,7 @@ def test_file_explorer_txt_only_glob(self, tmpdir): (Path(tmpdir) / "foo" / "img.png").touch() (Path(tmpdir) / "foo" / "bar" / "bar.txt").touch() - file_explorer = gr.FileExplorer(glob="*.txt", root=Path(tmpdir)) + file_explorer = gr.FileExplorer(glob="*.txt", root_dir=Path(tmpdir)) tree = file_explorer.ls(["foo"]) answer = [ diff --git a/test/conftest.py b/test/conftest.py index 7a4386e5efdcf..16b0efebb56f1 100644 --- a/test/conftest.py +++ b/test/conftest.py @@ -29,7 +29,6 @@ def io_components(): gr.components.FormComponent, gr.State, gr.LoginButton, - gr.LogoutButton, gr.Timer, ]: continue diff --git a/test/test_blocks.py b/test/test_blocks.py index f9688d26c6fe0..07bfdbc2d8804 100644 --- a/test/test_blocks.py +++ b/test/test_blocks.py @@ -331,7 +331,7 @@ def generator_function(): greet_btn.click(lambda: "Hello!", inputs=None, outputs=[greeting]) generator_btn.click(generator_function, inputs=None, outputs=[counter]) - demo.load(continuous_fn, inputs=None, outputs=[meaning_of_life], every=1) + demo.load(continuous_fn, inputs=None, outputs=[meaning_of_life]) dependencies = demo.config["dependencies"] assert dependencies[0]["types"] == { @@ -350,10 +350,6 @@ def generator_function(): "generator": False, "cancel": False, } - assert dependencies[4]["types"] == { - "generator": False, - "cancel": False, - } @patch( "gradio.themes.ThemeClass.from_hub", @@ -1632,13 +1628,6 @@ def test_recover_kwargs(): assert props == {"format": "wav", "autoplay": False} -def test_deprecation_warning_emitted_when_concurrency_count_set(): - with pytest.raises(DeprecationWarning): - gr.Interface(lambda x: x, gr.Textbox(), gr.Textbox()).queue( - concurrency_count=12 - ) - - def test_postprocess_update_dict(): block = gr.Textbox() update_dict = {"value": 2.0, "visible": True, "invalid_arg": "hello"} diff --git a/test/test_buttons.py b/test/test_buttons.py index 042fbb2ccb49f..039ae8c4c3f88 100644 --- a/test/test_buttons.py +++ b/test/test_buttons.py @@ -35,12 +35,6 @@ def test_login_button_warns_when_not_on_spaces(self): with gr.Blocks(): gr.LoginButton() - @pytest.mark.flaky - def test_logout_button_warns_when_not_on_spaces(self): - with pytest.warns(UserWarning): - with gr.Blocks(): - gr.LogoutButton() - @patch("gradio.oauth.get_space", lambda: "fake_space") @patch("gradio.oauth._add_oauth_routes") def test_login_button_setup_correctly(self, mock_add_oauth_routes): diff --git a/test/test_flagging.py b/test/test_flagging.py index 4bd41d25d558f..ca7ebda74e88f 100644 --- a/test/test_flagging.py +++ b/test/test_flagging.py @@ -1,7 +1,7 @@ import os import pathlib import tempfile -from unittest.mock import MagicMock, patch +from unittest.mock import MagicMock import pytest @@ -66,84 +66,6 @@ def test_simple_csv_flagging_callback(self): io.close() -class TestHuggingFaceDatasetSaver: - @patch( - "huggingface_hub.create_repo", - return_value=MagicMock(repo_id="gradio-tests/test"), - ) - @patch("huggingface_hub.hf_hub_download") - @patch("huggingface_hub.metadata_update") - def test_saver_setup(self, metadata_update, mock_download, mock_create): - flagger = flagging.HuggingFaceDatasetSaver("test_token", "test") - with tempfile.TemporaryDirectory() as tmpdirname: - flagger.setup([gr.Audio, gr.Textbox], tmpdirname) - mock_create.assert_called_once() - mock_download.assert_called() - - @patch( - "huggingface_hub.create_repo", - return_value=MagicMock(repo_id="gradio-tests/test"), - ) - @patch("huggingface_hub.hf_hub_download") - @patch("huggingface_hub.upload_folder") - @patch("huggingface_hub.upload_file") - @patch("huggingface_hub.metadata_update") - def test_saver_flag_same_dir( - self, metadata_update, mock_upload_file, mock_upload, mock_download, mock_create - ): - with tempfile.TemporaryDirectory() as tmpdirname: - io = gr.Interface( - lambda x: x, - "text", - "text", - flagging_dir=tmpdirname, - flagging_callback=flagging.HuggingFaceDatasetSaver("test", "test"), - ) - row_count = io.flagging_callback.flag(["test", "test"], "") - assert row_count == 1 # 2 rows written including header - row_count = io.flagging_callback.flag(["test", "test"]) - assert row_count == 2 # 3 rows written including header - for _, _, filenames in os.walk(tmpdirname): - for f in filenames: - fname = os.path.basename(f) - assert fname in ["data.csv", "dataset_info.json"] or fname.endswith( - ".lock" - ) - - @patch( - "huggingface_hub.create_repo", - return_value=MagicMock(repo_id="gradio-tests/test"), - ) - @patch("huggingface_hub.hf_hub_download") - @patch("huggingface_hub.upload_folder") - @patch("huggingface_hub.upload_file") - @patch("huggingface_hub.metadata_update") - def test_saver_flag_separate_dirs( - self, metadata_update, mock_upload_file, mock_upload, mock_download, mock_create - ): - with tempfile.TemporaryDirectory() as tmpdirname: - io = gr.Interface( - lambda x: x, - "text", - "text", - flagging_dir=tmpdirname, - flagging_callback=flagging.HuggingFaceDatasetSaver( - "test", "test", separate_dirs=True - ), - ) - row_count = io.flagging_callback.flag(["test", "test"], "") - assert row_count == 1 # 2 rows written including header - row_count = io.flagging_callback.flag(["test", "test"]) - assert row_count == 2 # 3 rows written including header - for _, _, filenames in os.walk(tmpdirname): - for f in filenames: - fname = os.path.basename(f) - assert fname in [ - "metadata.jsonl", - "dataset_info.json", - ] or fname.endswith(".lock") - - class TestDisableFlagging: def test_flagging_no_permission_error_with_flagging_disabled(self): tmpdirname = tempfile.mkdtemp() diff --git a/test/test_routes.py b/test/test_routes.py index 282f71068fd39..515ba3e40bc3e 100644 --- a/test/test_routes.py +++ b/test/test_routes.py @@ -1003,7 +1003,7 @@ def test_show_api_true_when_is_wasm_false(self): def test_component_server_endpoints(connect): here = os.path.dirname(os.path.abspath(__file__)) with gr.Blocks() as demo: - file_explorer = gr.FileExplorer(root=here) + file_explorer = gr.FileExplorer(root_dir=here) with closing(demo) as io: app, _, _ = io.launch(prevent_thread_lock=True) From 58a5eadd6127a22eb78ac8f441274f022d9f920f Mon Sep 17 00:00:00 2001 From: pngwn Date: Thu, 18 Jul 2024 10:56:03 +0100 Subject: [PATCH 003/195] fix (#8830) --- .config/.prettierignore | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.config/.prettierignore b/.config/.prettierignore index 40c07570847cf..e77b34d973b64 100644 --- a/.config/.prettierignore +++ b/.config/.prettierignore @@ -31,4 +31,5 @@ sweep.yaml **/theme/src/pollen.css **/venv/** ../js/app/src/api_docs/CodeSnippet.svelte -../js/app/src/api_docs/RecordingSnippet.svelte \ No newline at end of file +../js/app/src/api_docs/RecordingSnippet.svelte +.changeset/pre.json \ No newline at end of file From a7bc58fca7b513e3593dc277a68042a50107799c Mon Sep 17 00:00:00 2001 From: pngwn Date: Thu, 18 Jul 2024 11:04:04 +0100 Subject: [PATCH 004/195] fix --- .config/.prettierignore | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.config/.prettierignore b/.config/.prettierignore index e77b34d973b64..d576881477953 100644 --- a/.config/.prettierignore +++ b/.config/.prettierignore @@ -32,4 +32,4 @@ sweep.yaml **/venv/** ../js/app/src/api_docs/CodeSnippet.svelte ../js/app/src/api_docs/RecordingSnippet.svelte -.changeset/pre.json \ No newline at end of file +/.changeset/pre.json \ No newline at end of file From 4cf8af9407a44ee914e0be567da38b29f00eff8e Mon Sep 17 00:00:00 2001 From: Abubakar Abid Date: Fri, 19 Jul 2024 00:23:22 -0700 Subject: [PATCH 005/195] Prevent invalid values from being submitted to dropdown, etc. (#8810) * prevent invalid values * error * add changeset * component * add tests * fix tests * spec ts * format --------- Co-authored-by: gradio-pr-bot --- .changeset/dark-cougars-fold.md | 5 + .changeset/pre.json | 144 ++++++++++++------------- demo/blocks_essay/run.ipynb | 2 +- demo/blocks_essay/run.py | 5 + gradio/components/checkboxgroup.py | 8 +- gradio/components/dropdown.py | 24 +++-- gradio/components/radio.py | 18 ++-- js/app/test/blocks_essay.spec.ts | 3 + test/components/test_checkbox_group.py | 5 +- test/components/test_dropdown.py | 21 ++-- test/components/test_radio.py | 3 +- 11 files changed, 143 insertions(+), 95 deletions(-) create mode 100644 .changeset/dark-cougars-fold.md diff --git a/.changeset/dark-cougars-fold.md b/.changeset/dark-cougars-fold.md new file mode 100644 index 0000000000000..3c3288fce0325 --- /dev/null +++ b/.changeset/dark-cougars-fold.md @@ -0,0 +1,5 @@ +--- +"gradio": minor +--- + +feat:Prevent invalid values from being submitted to dropdown, etc. diff --git a/.changeset/pre.json b/.changeset/pre.json index 3228d41bbe5c4..89cb82454a8fc 100644 --- a/.changeset/pre.json +++ b/.changeset/pre.json @@ -1,74 +1,74 @@ { - "mode": "pre", - "tag": "beta", - "initialVersions": { - "@gradio/client": "1.3.0", - "gradio_client": "1.1.0", - "gradio": "4.38.1", - "@gradio/cdn-test": "0.0.1", - "@gradio/spaces-test": "0.0.1", - "website": "0.34.0", - "@gradio/accordion": "0.3.18", - "@gradio/annotatedimage": "0.6.13", - "@gradio/app": "1.38.1", - "@gradio/atoms": "0.7.6", - "@gradio/audio": "0.12.2", - "@gradio/box": "0.1.20", - "@gradio/button": "0.2.46", - "@gradio/chatbot": "0.12.1", - "@gradio/checkbox": "0.3.8", - "@gradio/checkboxgroup": "0.5.8", - "@gradio/code": "0.7.0", - "@gradio/colorpicker": "0.3.8", - "@gradio/column": "0.1.2", - "@gradio/dataframe": "0.8.13", - "@gradio/dataset": "0.2.0", - "@gradio/datetime": "0.0.2", - "@gradio/downloadbutton": "0.1.23", - "@gradio/dropdown": "0.7.8", - "@gradio/fallback": "0.3.8", - "@gradio/file": "0.8.5", - "@gradio/fileexplorer": "0.4.14", - "@gradio/form": "0.1.20", - "@gradio/gallery": "0.11.2", - "@gradio/group": "0.1.1", - "@gradio/highlightedtext": "0.7.2", - "@gradio/html": "0.3.1", - "@gradio/icons": "0.6.0", - "@gradio/image": "0.12.2", - "@gradio/imageeditor": "0.7.13", - "@gradio/json": "0.2.8", - "@gradio/label": "0.3.8", - "@gradio/lite": "4.38.1", - "@gradio/markdown": "0.8.1", - "@gradio/model3d": "0.11.0", - "@gradio/multimodaltextbox": "0.5.2", - "@gradio/number": "0.4.8", - "@gradio/paramviewer": "0.4.17", - "@gradio/plot": "0.6.0", - "@gradio/preview": "0.10.1", - "gradio_test": "0.5.0", - "@gradio/radio": "0.5.8", - "@gradio/row": "0.1.3", - "@gradio/simpledropdown": "0.2.8", - "@gradio/simpleimage": "0.6.2", - "@gradio/simpletextbox": "0.2.8", - "@gradio/slider": "0.4.8", - "@gradio/state": "0.1.0", - "@gradio/statustracker": "0.7.1", - "@gradio/storybook": "0.6.0", - "@gradio/tabitem": "0.2.12", - "@gradio/tabs": "0.2.11", - "@gradio/textbox": "0.6.7", - "@gradio/theme": "0.2.3", - "@gradio/timer": "0.3.0", - "@gradio/tooltip": "0.1.0", - "@gradio/tootils": "0.6.0", - "@gradio/upload": "0.11.5", - "@gradio/uploadbutton": "0.6.14", - "@gradio/utils": "0.5.1", - "@gradio/video": "0.9.2", - "@gradio/wasm": "0.11.0" - }, - "changesets": [] + "mode": "pre", + "tag": "beta", + "initialVersions": { + "@gradio/client": "1.3.0", + "gradio_client": "1.1.0", + "gradio": "4.38.1", + "@gradio/cdn-test": "0.0.1", + "@gradio/spaces-test": "0.0.1", + "website": "0.34.0", + "@gradio/accordion": "0.3.18", + "@gradio/annotatedimage": "0.6.13", + "@gradio/app": "1.38.1", + "@gradio/atoms": "0.7.6", + "@gradio/audio": "0.12.2", + "@gradio/box": "0.1.20", + "@gradio/button": "0.2.46", + "@gradio/chatbot": "0.12.1", + "@gradio/checkbox": "0.3.8", + "@gradio/checkboxgroup": "0.5.8", + "@gradio/code": "0.7.0", + "@gradio/colorpicker": "0.3.8", + "@gradio/column": "0.1.2", + "@gradio/dataframe": "0.8.13", + "@gradio/dataset": "0.2.0", + "@gradio/datetime": "0.0.2", + "@gradio/downloadbutton": "0.1.23", + "@gradio/dropdown": "0.7.8", + "@gradio/fallback": "0.3.8", + "@gradio/file": "0.8.5", + "@gradio/fileexplorer": "0.4.14", + "@gradio/form": "0.1.20", + "@gradio/gallery": "0.11.2", + "@gradio/group": "0.1.1", + "@gradio/highlightedtext": "0.7.2", + "@gradio/html": "0.3.1", + "@gradio/icons": "0.6.0", + "@gradio/image": "0.12.2", + "@gradio/imageeditor": "0.7.13", + "@gradio/json": "0.2.8", + "@gradio/label": "0.3.8", + "@gradio/lite": "4.38.1", + "@gradio/markdown": "0.8.1", + "@gradio/model3d": "0.11.0", + "@gradio/multimodaltextbox": "0.5.2", + "@gradio/number": "0.4.8", + "@gradio/paramviewer": "0.4.17", + "@gradio/plot": "0.6.0", + "@gradio/preview": "0.10.1", + "gradio_test": "0.5.0", + "@gradio/radio": "0.5.8", + "@gradio/row": "0.1.3", + "@gradio/simpledropdown": "0.2.8", + "@gradio/simpleimage": "0.6.2", + "@gradio/simpletextbox": "0.2.8", + "@gradio/slider": "0.4.8", + "@gradio/state": "0.1.0", + "@gradio/statustracker": "0.7.1", + "@gradio/storybook": "0.6.0", + "@gradio/tabitem": "0.2.12", + "@gradio/tabs": "0.2.11", + "@gradio/textbox": "0.6.7", + "@gradio/theme": "0.2.3", + "@gradio/timer": "0.3.0", + "@gradio/tooltip": "0.1.0", + "@gradio/tootils": "0.6.0", + "@gradio/upload": "0.11.5", + "@gradio/uploadbutton": "0.6.14", + "@gradio/utils": "0.5.1", + "@gradio/video": "0.9.2", + "@gradio/wasm": "0.11.0" + }, + "changesets": [] } diff --git a/demo/blocks_essay/run.ipynb b/demo/blocks_essay/run.ipynb index b5d25834b039d..45d6ff2edc658 100644 --- a/demo/blocks_essay/run.ipynb +++ b/demo/blocks_essay/run.ipynb @@ -1 +1 @@ -{"cells": [{"cell_type": "markdown", "id": "302934307671667531413257853548643485645", "metadata": {}, "source": ["# Gradio Demo: blocks_essay"]}, {"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 gradio as gr\n", "\n", "countries_cities_dict = {\n", " \"USA\": [\"New York\", \"Los Angeles\", \"Chicago\"],\n", " \"Canada\": [\"Toronto\", \"Montreal\", \"Vancouver\"],\n", " \"Pakistan\": [\"Karachi\", \"Lahore\", \"Islamabad\"],\n", "}\n", "\n", "\n", "def change_textbox(choice):\n", " if choice == \"short\":\n", " return gr.Textbox(lines=2, visible=True), gr.Button(interactive=True)\n", " elif choice == \"long\":\n", " return gr.Textbox(lines=8, visible=True, value=\"Lorem ipsum dolor sit amet\"), gr.Button(interactive=True)\n", " else:\n", " return gr.Textbox(visible=False), gr.Button(interactive=False)\n", "\n", "\n", "with gr.Blocks() as demo:\n", " radio = gr.Radio(\n", " [\"short\", \"long\", \"none\"], label=\"What kind of essay would you like to write?\"\n", " )\n", " text = gr.Textbox(lines=2, interactive=True, show_copy_button=True)\n", "\n", " with gr.Row():\n", " num = gr.Number(minimum=0, maximum=100, label=\"input\")\n", " out = gr.Number(label=\"output\")\n", " minimum_slider = gr.Slider(0, 100, 0, label=\"min\")\n", " maximum_slider = gr.Slider(0, 100, 100, label=\"max\")\n", " submit_btn = gr.Button(\"Submit\", variant=\"primary\")\n", "\n", " with gr.Row():\n", " country = gr.Dropdown(list(countries_cities_dict.keys()), label=\"Country\")\n", " cities = gr.Dropdown([], label=\"Cities\")\n", " \n", " @country.change(inputs=country, outputs=cities)\n", " def update_cities(country):\n", " cities = list(countries_cities_dict[country])\n", " return gr.Dropdown(choices=cities, value=cities[0], interactive=True)\n", "\n", " def reset_bounds(minimum, maximum):\n", " return gr.Number(minimum=minimum, maximum=maximum)\n", "\n", " radio.change(fn=change_textbox, inputs=radio, outputs=[text, submit_btn])\n", " gr.on(\n", " [minimum_slider.change, maximum_slider.change],\n", " reset_bounds,\n", " [minimum_slider, maximum_slider],\n", " outputs=num,\n", " )\n", " num.submit(lambda x: x, num, out)\n", "\n", "\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: blocks_essay"]}, {"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 gradio as gr\n", "\n", "countries_cities_dict = {\n", " \"USA\": [\"New York\", \"Los Angeles\", \"Chicago\"],\n", " \"Canada\": [\"Toronto\", \"Montreal\", \"Vancouver\"],\n", " \"Pakistan\": [\"Karachi\", \"Lahore\", \"Islamabad\"],\n", "}\n", "\n", "\n", "def change_textbox(choice):\n", " if choice == \"short\":\n", " return gr.Textbox(lines=2, visible=True), gr.Button(interactive=True)\n", " elif choice == \"long\":\n", " return gr.Textbox(lines=8, visible=True, value=\"Lorem ipsum dolor sit amet\"), gr.Button(interactive=True)\n", " else:\n", " return gr.Textbox(visible=False), gr.Button(interactive=False)\n", "\n", "\n", "with gr.Blocks() as demo:\n", " radio = gr.Radio(\n", " [\"short\", \"long\", \"none\"], label=\"What kind of essay would you like to write?\"\n", " )\n", " text = gr.Textbox(lines=2, interactive=True, show_copy_button=True)\n", "\n", " with gr.Row():\n", " num = gr.Number(minimum=0, maximum=100, label=\"input\")\n", " out = gr.Number(label=\"output\")\n", " minimum_slider = gr.Slider(0, 100, 0, label=\"min\")\n", " maximum_slider = gr.Slider(0, 100, 100, label=\"max\")\n", " submit_btn = gr.Button(\"Submit\", variant=\"primary\")\n", "\n", " with gr.Row():\n", " country = gr.Dropdown(list(countries_cities_dict.keys()), label=\"Country\")\n", " cities = gr.Dropdown([], label=\"Cities\")\n", " first_letter = gr.Textbox(label=\"First Letter\")\n", " \n", " @country.change(inputs=country, outputs=cities)\n", " def update_cities(country):\n", " cities = list(countries_cities_dict[country])\n", " return gr.Dropdown(choices=cities, value=cities[0], interactive=True)\n", "\n", " def reset_bounds(minimum, maximum):\n", " return gr.Number(minimum=minimum, maximum=maximum)\n", "\n", " radio.change(fn=change_textbox, inputs=radio, outputs=[text, submit_btn])\n", " gr.on(\n", " [minimum_slider.change, maximum_slider.change],\n", " reset_bounds,\n", " [minimum_slider, maximum_slider],\n", " outputs=num,\n", " )\n", " num.submit(lambda x: x, num, out)\n", "\n", " cities.change(\n", " lambda x: x[0], cities, first_letter\n", " )\n", "\n", "\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/blocks_essay/run.py b/demo/blocks_essay/run.py index 2b2a313a9156c..12bd17044f3a4 100644 --- a/demo/blocks_essay/run.py +++ b/demo/blocks_essay/run.py @@ -32,6 +32,7 @@ def change_textbox(choice): with gr.Row(): country = gr.Dropdown(list(countries_cities_dict.keys()), label="Country") cities = gr.Dropdown([], label="Cities") + first_letter = gr.Textbox(label="First Letter") @country.change(inputs=country, outputs=cities) def update_cities(country): @@ -50,6 +51,10 @@ def reset_bounds(minimum, maximum): ) num.submit(lambda x: x, num, out) + cities.change( + lambda x: x[0], cities, first_letter + ) + if __name__ == "__main__": diff --git a/gradio/components/checkboxgroup.py b/gradio/components/checkboxgroup.py index a9601b92801dc..6541388589e0b 100644 --- a/gradio/components/checkboxgroup.py +++ b/gradio/components/checkboxgroup.py @@ -8,6 +8,7 @@ from gradio.components.base import Component, FormComponent from gradio.events import Events +from gradio.exceptions import Error if TYPE_CHECKING: from gradio.components import Timer @@ -116,10 +117,15 @@ def preprocess( Returns: Passes the list of checked checkboxes as a `list[str | int | float]` or their indices as a `list[int]` into the function, depending on `type`. """ + choice_values = [value for _, value in self.choices] + for value in payload: + if value not in choice_values: + raise Error( + f"Value: {value} is not in the list of choices: {choice_values}" + ) if self.type == "value": return payload elif self.type == "index": - choice_values = [value for _, value in self.choices] return [ choice_values.index(choice) if choice in choice_values else None for choice in payload diff --git a/gradio/components/dropdown.py b/gradio/components/dropdown.py index d44374993c8dd..e19031c6fca6f 100644 --- a/gradio/components/dropdown.py +++ b/gradio/components/dropdown.py @@ -9,6 +9,7 @@ from gradio.components.base import Component, FormComponent from gradio.events import Events +from gradio.exceptions import Error if TYPE_CHECKING: from gradio.components import Timer @@ -159,15 +160,26 @@ def preprocess( Returns: Passes the value of the selected dropdown choice as a `str | int | float` or its index as an `int` into the function, depending on `type`. Or, if `multiselect` is True, passes the values of the selected dropdown choices as a list of correspoding values/indices instead. """ + if payload is None: + return None + + choice_values = [value for _, value in self.choices] + if not self.allow_custom_value: + if isinstance(payload, list): + for value in payload: + if value not in choice_values: + raise Error( + f"Value: {value} is not in the list of choices: {choice_values}" + ) + elif payload not in choice_values: + raise Error( + f"Value: {payload} is not in the list of choices: {choice_values}" + ) + if self.type == "value": return payload elif self.type == "index": - choice_values = [value for _, value in self.choices] - if payload is None: - return None - elif self.multiselect: - if not isinstance(payload, list): - raise TypeError("Multiselect dropdown payload must be a list") + if isinstance(payload, list): return [ choice_values.index(choice) if choice in choice_values else None for choice in payload diff --git a/gradio/components/radio.py b/gradio/components/radio.py index 4804c175db5e9..e9910f0327ae3 100644 --- a/gradio/components/radio.py +++ b/gradio/components/radio.py @@ -8,6 +8,7 @@ from gradio.components.base import Component, FormComponent from gradio.events import Events +from gradio.exceptions import Error if TYPE_CHECKING: from gradio.components import Timer @@ -108,16 +109,19 @@ def preprocess(self, payload: str | int | float | None) -> str | int | float | N Returns: Passes the value of the selected radio button as a `str | int | float`, or its index as an `int` into the function, depending on `type`. """ + if payload is None: + return None + + choice_values = [value for _, value in self.choices] + if payload not in choice_values: + raise Error( + f"Value: {payload} is not in the list of choices: {choice_values}" + ) + if self.type == "value": return payload elif self.type == "index": - if payload is None: - return None - else: - choice_values = [value for _, value in self.choices] - return ( - choice_values.index(payload) if payload in choice_values else None - ) + return choice_values.index(payload) else: raise ValueError( f"Unknown type: {self.type}. Please choose from: 'value', 'index'." diff --git a/js/app/test/blocks_essay.spec.ts b/js/app/test/blocks_essay.spec.ts index 1dc3ded0d6337..036c392eae288 100644 --- a/js/app/test/blocks_essay.spec.ts +++ b/js/app/test/blocks_essay.spec.ts @@ -54,12 +54,15 @@ test("updates backend correctly", async ({ page }) => { test("updates dropdown choices correctly", async ({ page }) => { const country = await page.getByLabel("Country").first(); const city = await page.getByLabel("Cities").first(); + const first_letter = await page.getByLabel("First Letter").first(); await country.fill("Canada"); await country.press("Enter"); await expect(city).toHaveValue("Toronto"); + await expect(first_letter).toHaveValue("T"); await country.fill("Pakistan"); await country.press("Enter"); await expect(city).toHaveValue("Karachi"); + await expect(first_letter).toHaveValue("K"); }); diff --git a/test/components/test_checkbox_group.py b/test/components/test_checkbox_group.py index c88f2146d81ca..0484fa73c4126 100644 --- a/test/components/test_checkbox_group.py +++ b/test/components/test_checkbox_group.py @@ -11,11 +11,14 @@ def test_component_functions(self): checkboxes_input = gr.CheckboxGroup(["a", "b", "c"]) assert checkboxes_input.preprocess(["a", "c"]) == ["a", "c"] assert checkboxes_input.postprocess(["a", "c"]) == ["a", "c"] + with pytest.raises(gr.Error): + checkboxes_input.preprocess(["d"]) checkboxes_input = gr.CheckboxGroup(["a", "b"], type="index") assert checkboxes_input.preprocess(["a"]) == [0] assert checkboxes_input.preprocess(["a", "b"]) == [0, 1] - assert checkboxes_input.preprocess(["a", "b", "c"]) == [0, 1, None] + with pytest.raises(gr.Error): + checkboxes_input.preprocess(["a", "b", "c"]) # When a Gradio app is loaded with gr.load, the tuples are converted to lists, # so we need to test that case as well diff --git a/test/components/test_dropdown.py b/test/components/test_dropdown.py index 2e170d102fbf9..c16d171b931a0 100644 --- a/test/components/test_dropdown.py +++ b/test/components/test_dropdown.py @@ -14,20 +14,29 @@ def test_component_functions(self): assert dropdown_input.preprocess("c full") == "c full" assert dropdown_input.postprocess("c full") == ["c full"] - # When a Gradio app is loaded with gr.load, the tuples are converted to lists, - # so we need to test that case as well - dropdown_input = gr.Dropdown(["a", "b", ["c", "c full"]]) # type: ignore - assert dropdown_input.choices == [("a", "a"), ("b", "b"), ("c", "c full")] + # When an external Gradio app is loaded with gr.load, the tuples are converted to lists, + # so we test that case as well + dropdown = gr.Dropdown(["a", "b", ["c", "c full"]]) # type: ignore + assert dropdown.choices == [("a", "a"), ("b", "b"), ("c", "c full")] dropdown = gr.Dropdown(choices=["a", "b"], type="index") assert dropdown.preprocess("a") == 0 assert dropdown.preprocess("b") == 1 - assert dropdown.preprocess("c") is None + with pytest.raises(gr.Error): + dropdown.preprocess("c") dropdown = gr.Dropdown(choices=["a", "b"], type="index", multiselect=True) assert dropdown.preprocess(["a"]) == [0] assert dropdown.preprocess(["a", "b"]) == [0, 1] - assert dropdown.preprocess(["a", "b", "c"]) == [0, 1, None] + with pytest.raises(gr.Error): + dropdown.preprocess(["a", "b", "c"]) + + dropdown = gr.Dropdown(["a", "b"], allow_custom_value=True) + assert dropdown.preprocess("a") == "a" + assert dropdown.preprocess("c") == "c" + dropdown = gr.Dropdown(["a", "b"], allow_custom_value=True, type="index") + assert dropdown.preprocess("a") == 0 + assert dropdown.preprocess("c") is None dropdown_input_multiselect = gr.Dropdown(["a", "b", ("c", "c full")]) assert dropdown_input_multiselect.preprocess(["a", "c full"]) == ["a", "c full"] diff --git a/test/components/test_radio.py b/test/components/test_radio.py index 1cdeec4b5d8e5..4995fee4f26d5 100644 --- a/test/components/test_radio.py +++ b/test/components/test_radio.py @@ -38,7 +38,8 @@ def test_component_functions(self): radio = gr.Radio(choices=["a", "b"], type="index") assert radio.preprocess("a") == 0 assert radio.preprocess("b") == 1 - assert radio.preprocess("c") is None + with pytest.raises(gr.Error): + radio.preprocess("c") # When a Gradio app is loaded with gr.load, the tuples are converted to lists, # so we need to test that case as well From dfc006ecc732aea933f428292e8443a0e498e20a Mon Sep 17 00:00:00 2001 From: Abubakar Abid Date: Tue, 23 Jul 2024 15:28:22 -0700 Subject: [PATCH 006/195] fixes --- client/python/test/test_client.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/client/python/test/test_client.py b/client/python/test/test_client.py index 016e165354cc0..4bb9120b5bac4 100644 --- a/client/python/test/test_client.py +++ b/client/python/test/test_client.py @@ -24,7 +24,7 @@ from gradio_client import Client, handle_file from gradio_client.client import DEFAULT_TEMP_DIR -from gradio_client.exceptions import AuthenticationError +from gradio_client.exceptions import AppError, AuthenticationError from gradio_client.utils import ( Communicator, ProgressUnit, @@ -1397,7 +1397,7 @@ def test_add_secrets(self, mock_time, mock_init, mock_duplicate, mock_add_secret token=HF_TOKEN, ) - + def test_upstream_exceptions(count_generator_demo_exception): with connect(count_generator_demo_exception, show_error=True) as client: with pytest.raises( From 3408dba7560a17371be679d0f01564a5606dc90b Mon Sep 17 00:00:00 2001 From: Abubakar Abid Date: Mon, 29 Jul 2024 15:11:10 -0700 Subject: [PATCH 007/195] Remove manual ip address check and launch counter (#8884) * changes * add changeset * hash * changes * remove * changes * rename * internal * changes * remove json path * merge * fix tests --------- Co-authored-by: gradio-pr-bot --- .changeset/weak-glasses-enter.md | 5 +++ client/python/test/test_client.py | 17 +--------- gradio/analytics.py | 52 ++----------------------------- gradio/blocks.py | 1 - gradio/context.py | 1 - gradio/utils.py | 22 ------------- test/test_analytics.py | 26 ---------------- 7 files changed, 8 insertions(+), 116 deletions(-) create mode 100644 .changeset/weak-glasses-enter.md diff --git a/.changeset/weak-glasses-enter.md b/.changeset/weak-glasses-enter.md new file mode 100644 index 0000000000000..0e76c994d10d8 --- /dev/null +++ b/.changeset/weak-glasses-enter.md @@ -0,0 +1,5 @@ +--- +"gradio": minor +--- + +feat:replace ip addresses with machine-specific hashes diff --git a/client/python/test/test_client.py b/client/python/test/test_client.py index 4bb9120b5bac4..9cdf5ab8a3d8d 100644 --- a/client/python/test/test_client.py +++ b/client/python/test/test_client.py @@ -24,7 +24,7 @@ from gradio_client import Client, handle_file from gradio_client.client import DEFAULT_TEMP_DIR -from gradio_client.exceptions import AppError, AuthenticationError +from gradio_client.exceptions import AuthenticationError from gradio_client.utils import ( Communicator, ProgressUnit, @@ -1398,21 +1398,6 @@ def test_add_secrets(self, mock_time, mock_init, mock_duplicate, mock_add_secret ) -def test_upstream_exceptions(count_generator_demo_exception): - with connect(count_generator_demo_exception, show_error=True) as client: - with pytest.raises( - AppError, match="The upstream Gradio app has raised an exception: Oh no!" - ): - client.predict(7, api_name="/count") - - with connect(count_generator_demo_exception) as client: - with pytest.raises( - AppError, - match="The upstream Gradio app has raised an exception but has not enabled verbose error reporting.", - ): - client.predict(7, api_name="/count") - - def test_httpx_kwargs(increment_demo): with connect( increment_demo, client_kwargs={"httpx_kwargs": {"timeout": 5}} diff --git a/gradio/analytics.py b/gradio/analytics.py index 79fb9c2d54ed1..6ec38c386a14d 100644 --- a/gradio/analytics.py +++ b/gradio/analytics.py @@ -16,7 +16,6 @@ import gradio from gradio import wasm_utils -from gradio.context import Context from gradio.utils import core_gradio_components, get_package_version # For testability, we import the pyfetch function into this module scope and define a fallback coroutine object to be patched in tests. @@ -67,7 +66,7 @@ def _do_analytics_request(topic: str, data: dict[str, Any]) -> None: def _do_normal_analytics_request(topic: str, data: dict[str, Any]) -> None: - data["ip_address"] = get_local_ip_address() + data["ip_address"] = "" try: _send_telemetry_in_thread( topic=topic, @@ -80,7 +79,7 @@ def _do_normal_analytics_request(topic: str, data: dict[str, Any]) -> None: async def _do_wasm_analytics_request(url: str, data: dict[str, Any]) -> None: - data["ip_address"] = await get_local_ip_address_wasm() + data["ip_address"] = "" # We use urllib.parse.urlencode to encode the data as a form. # Ref: https://docs.python.org/3/library/urllib.request.html#urllib-examples @@ -116,53 +115,6 @@ def version_check(): pass -def get_local_ip_address() -> str: - """ - Gets the public IP address or returns the string "No internet connection" if unable - to obtain it or the string "Analytics disabled" if a user has disabled analytics. - Does not make a new request if the IP address has already been obtained in the - same Python session. - """ - if not analytics_enabled(): - return "Analytics disabled" - - if Context.ip_address is None: - try: - ip_address = httpx.get( - "https://checkip.amazonaws.com/", timeout=3 - ).text.strip() - except (httpx.ConnectError, httpx.ReadTimeout): - ip_address = "No internet connection" - Context.ip_address = ip_address - else: - ip_address = Context.ip_address - return ip_address - - -async def get_local_ip_address_wasm() -> str: - """The Wasm-compatible version of get_local_ip_address().""" - if not analytics_enabled(): - return "Analytics disabled" - - if Context.ip_address is None: - try: - response = await asyncio.wait_for( - pyodide_pyfetch( - # The API used by the normal version (`get_local_ip_address()`), `https://checkip.amazonaws.com/``, blocks CORS requests, so here we use a different API. - "https://api.ipify.org" - ), - timeout=5, - ) - response_text: str = await response.string() # type: ignore - ip_address = response_text.strip() - except (asyncio.TimeoutError, OSError): - ip_address = "No internet connection" - Context.ip_address = ip_address - else: - ip_address = Context.ip_address - return ip_address - - def initiated_analytics(data: dict[str, Any]) -> None: if not analytics_enabled(): return diff --git a/gradio/blocks.py b/gradio/blocks.py index a3dda94c79e78..aa933f18190f4 100644 --- a/gradio/blocks.py +++ b/gradio/blocks.py @@ -2392,7 +2392,6 @@ def reverse(text): # So we need to manually cancel them. See `self.close()`.. self.startup_events() - utils.launch_counter() self.is_sagemaker = utils.sagemaker_check() if share is None: if self.is_colab: diff --git a/gradio/context.py b/gradio/context.py index b754fd056aad1..42974c3f7e90d 100644 --- a/gradio/context.py +++ b/gradio/context.py @@ -16,7 +16,6 @@ class Context: root_block: Blocks | None = None # The current root block that holds all blocks. block: BlockContext | None = None # The current block that children are added to. id: int = 0 # Running id to uniquely refer to any block that gets defined - ip_address: str | None = None # The IP address of the user. hf_token: str | None = None # The token provided when loading private HF repos diff --git a/gradio/utils.py b/gradio/utils.py index 3eacbab67c0d4..eb65e08b91436 100644 --- a/gradio/utils.py +++ b/gradio/utils.py @@ -51,11 +51,9 @@ from gradio_client.documentation import document from typing_extensions import ParamSpec -import gradio from gradio.context import get_blocks_context from gradio.data_classes import BlocksConfigDict, FileData from gradio.exceptions import Error -from gradio.strings import en if TYPE_CHECKING: # Only import for type checking (is False at runtime). from gradio.blocks import BlockContext, Blocks @@ -63,8 +61,6 @@ from gradio.routes import App, Request from gradio.state_holder import SessionState -JSON_PATH = os.path.join(os.path.dirname(gradio.__file__), "launches.json") - P = ParamSpec("P") T = TypeVar("T") @@ -438,24 +434,6 @@ def download_if_url(article: str) -> str: return article -def launch_counter() -> None: - try: - if not os.path.exists(JSON_PATH): - launches = {"launches": 1} - with open(JSON_PATH, "w+", encoding="utf-8") as j: - json.dump(launches, j) - else: - with open(JSON_PATH, encoding="utf-8") as j: - launches = json.load(j) - launches["launches"] += 1 - if launches["launches"] in [25, 50, 150, 500, 1000]: - print(en["BETA_INVITE"]) - with open(JSON_PATH, "w", encoding="utf-8") as j: - j.write(json.dumps(launches)) - except Exception: - pass - - def get_default_args(func: Callable) -> list[Any]: signature = inspect.signature(func) return [ diff --git a/test/test_analytics.py b/test/test_analytics.py index a7ad729bdd542..f4bae64d3f565 100644 --- a/test/test_analytics.py +++ b/test/test_analytics.py @@ -1,15 +1,12 @@ import asyncio -import ipaddress import json import os import warnings from unittest.mock import patch -import httpx import pytest from gradio import analytics, wasm_utils -from gradio.context import Context os.environ["GRADIO_ANALYTICS_ENABLED"] = "False" @@ -62,26 +59,3 @@ async def test_error_analytics_successful_in_wasm_mode( await asyncio.wait(all_tasks) pyodide_pyfetch.assert_called() - - -class TestIPAddress: - @pytest.mark.flaky - def test_get_ip(self): - Context.ip_address = None - ip = analytics.get_local_ip_address() - if ip in ("No internet connection", "Analytics disabled"): - return - ipaddress.ip_address(ip) - - @patch("httpx.get") - def test_get_ip_without_internet(self, mock_get, monkeypatch): - mock_get.side_effect = httpx.ConnectError("Connection error") - monkeypatch.setenv("GRADIO_ANALYTICS_ENABLED", "True") - Context.ip_address = None - ip = analytics.get_local_ip_address() - assert ip == "No internet connection" - - monkeypatch.setenv("GRADIO_ANALYTICS_ENABLED", "False") - Context.ip_address = None - ip = analytics.get_local_ip_address() - assert ip == "Analytics disabled" From e0177064cafc4f81df0e16d36ea8afa511de0b5e Mon Sep 17 00:00:00 2001 From: Ali Abdalla Date: Tue, 30 Jul 2024 13:57:36 -0700 Subject: [PATCH 008/195] Remove deprecated documentation (#8940) * remove logoutbutton page * remove huggingfacedatasetsaver --- .../gradio/03_components/logoutbutton.svx | 78 ------------------- .../templates/gradio/other/01_flagging.svx | 56 ------------- 2 files changed, 134 deletions(-) delete mode 100644 js/_website/src/lib/templates/gradio/03_components/logoutbutton.svx diff --git a/js/_website/src/lib/templates/gradio/03_components/logoutbutton.svx b/js/_website/src/lib/templates/gradio/03_components/logoutbutton.svx deleted file mode 100644 index 155e0cbfc21af..0000000000000 --- a/js/_website/src/lib/templates/gradio/03_components/logoutbutton.svx +++ /dev/null @@ -1,78 +0,0 @@ - - - - -# {obj.name} - - -```python -gradio.LogoutButton(···) -``` - - -### Description -## {@html style_formatted_text(obj.description)} - - -### Behavior -## **As input component**: {@html style_formatted_text(obj.preprocess.return_doc.doc)} -##### Your function should accept one of these types: - -```python -def predict( - value: str | None -) - ... -``` - -
- -## **As output component**: {@html style_formatted_text(obj.postprocess.parameter_doc[0].doc)} -##### Your function should return one of these types: - -```python -def predict(···) -> str | None - ... - return value -``` - - - -### Initialization - - - -{#if obj.string_shortcuts && obj.string_shortcuts.length > 0} - -### Shortcuts - -{/if} - -{#if obj.demos && obj.demos.length > 0} - -### Demos - -{/if} - -{#if obj.fns && obj.fns.length > 0} - -### Event Listeners - -{/if} - -{#if obj.guides && obj.guides.length > 0} - -### Guides - -{/if} diff --git a/js/_website/src/lib/templates/gradio/other/01_flagging.svx b/js/_website/src/lib/templates/gradio/other/01_flagging.svx index d7773b95105d1..e919a34976940 100644 --- a/js/_website/src/lib/templates/gradio/other/01_flagging.svx +++ b/js/_website/src/lib/templates/gradio/other/01_flagging.svx @@ -11,7 +11,6 @@ let simple_csv_logger_obj = get_object("simplecsvlogger"); let csv_logger_obj = get_object("csvlogger"); - let hf_dataset_saver_obj = get_object("huggingfacedatasetsaver"); @@ -128,58 +127,3 @@ demo = gr.Interface(fn=image_classifier, inputs="image", outputs="label", {/if} - - - - -# {hf_dataset_saver_obj.name} - - - -```python -gradio.HuggingFaceDatasetSaver(hf_token, dataset_name, ···) -``` - - -### Description -## {@html style_formatted_text(hf_dataset_saver_obj.description)} - - - -{#if hf_dataset_saver_obj.example} -### Example Usage -```python -import gradio as gr -hf_writer = gr.HuggingFaceDatasetSaver(HF_API_TOKEN, "image-classification-mistakes") -def image_classifier(inp): - return {'cat': 0.3, 'dog': 0.7} -demo = gr.Interface(fn=image_classifier, inputs="image", outputs="label", - allow_flagging="manual", flagging_callback=hf_writer) -``` -{/if} - -{#if (hf_dataset_saver_obj.parameters.length > 0 && hf_dataset_saver_obj.parameters[0].name != "self") || hf_dataset_saver_obj.parameters.length > 1} - -### Initialization - -{/if} - -{#if hf_dataset_saver_obj.demos && hf_dataset_saver_obj.demos.length > 0} - -### Demos - -{/if} - -{#if hf_dataset_saver_obj.fns && hf_dataset_saver_obj.fns.length > 0} - -### Methods - -{/if} - -{#if hf_dataset_saver_obj.guides && hf_dataset_saver_obj.guides.length > 0} - -### Guides - -{/if} - - From 51b7a8b717060483145bf976569f43c5b09040ee Mon Sep 17 00:00:00 2001 From: Freddy Boulton Date: Wed, 31 Jul 2024 18:12:15 -0400 Subject: [PATCH 009/195] Use HTTP Livestreaming for audio/video streaming out (#8906) * HTTP live streaming * type check * fix code * Fix code * add code * Video demo * Fix tests * Update notebook * Add guide * Fix demo * Allow downloading * revert * Fix download filename * lint * notebooks * fix video demo * Fix config * Fix audio repeated play bug * Improve guide * fix audio? * Use cantina * Code * type check * add code * Use runtimeerror * Add code --- .github/workflows/test-functional.yml | 3 +- client/python/test/conftest.py | 24 +++++ demo/outbreak_forecast/requirements.txt | 3 +- demo/outbreak_forecast/run.ipynb | 2 +- demo/stream_audio_out/run.ipynb | 2 +- demo/stream_audio_out/run.py | 9 +- demo/stream_video_out/run.ipynb | 1 + demo/stream_video_out/run.py | 78 ++++++++++++++ .../compliment_bot_screen_recording_3x.mp4 | Bin 0 -> 222882 bytes gradio/blocks.py | 17 ++- gradio/components/audio.py | 52 +++++---- gradio/components/base.py | 11 +- gradio/components/video.py | 102 +++++++++++++++++- gradio/data_classes.py | 17 ++- gradio/helpers.py | 10 +- gradio/route_utils.py | 25 ++++- gradio/routes.py | 94 +++++++++++----- gradio/templates.py | 2 + .../02_streaming-outputs.md | 50 +++++++++ js/app/test/blocks_essay.spec.ts | 3 - js/app/test/stream_audio_out.spec.ts | 51 ++++++--- js/app/test/stream_video_out.spec.ts | 31 ++++++ js/audio/package.json | 5 +- js/audio/player/AudioPlayer.svelte | 75 +++++++++++-- js/audio/static/StaticAudio.svelte | 7 +- js/multimodaltextbox/Example.svelte | 2 +- js/video/Example.svelte | 1 + js/video/package.json | 5 +- js/video/shared/InteractiveVideo.svelte | 1 + js/video/shared/Player.svelte | 2 + js/video/shared/Video.svelte | 51 +++++++++ js/video/shared/VideoPreview.svelte | 8 +- pnpm-lock.yaml | 11 ++ scripts/copy_demos.py | 1 + test/components/test_video.py | 1 + test/test_helpers.py | 6 +- 36 files changed, 648 insertions(+), 115 deletions(-) create mode 100644 demo/stream_video_out/run.ipynb create mode 100644 demo/stream_video_out/run.py create mode 100644 demo/stream_video_out/video/compliment_bot_screen_recording_3x.mp4 create mode 100644 js/app/test/stream_video_out.spec.ts diff --git a/.github/workflows/test-functional.yml b/.github/workflows/test-functional.yml index 5ffac0fe38cbd..a3d6b6c7b4b29 100644 --- a/.github/workflows/test-functional.yml +++ b/.github/workflows/test-functional.yml @@ -51,11 +51,12 @@ jobs: with: always_install_pnpm: true build_lite: true - - name: install outbreak_forecast dependencies + - name: install demo dependencies run: | . venv/bin/activate python -m pip install -r demo/outbreak_forecast/requirements.txt python -m pip install -r demo/gradio_pdf_demo/requirements.txt + python -m pip install -r demo/stream_video_out/requirements.txt - run: pnpm exec playwright install chromium firefox - name: run browser tests run: | diff --git a/client/python/test/conftest.py b/client/python/test/conftest.py index 4e063c57d301c..b6a867b5e146f 100644 --- a/client/python/test/conftest.py +++ b/client/python/test/conftest.py @@ -238,6 +238,30 @@ def show(n): return demo +@pytest.fixture +def count_generator_demo_exception(): + def count(n): + for i in range(int(n)): + time.sleep(0.01) + if i == 5: + raise ValueError("Oh no!") + yield i + + def show(n): + return str(list(range(int(n)))) + + with gr.Blocks() as demo: + with gr.Column(): + num = gr.Number(value=10) + with gr.Row(): + count_btn = gr.Button("Count") + with gr.Column(): + out = gr.Textbox() + + count_btn.click(count, num, out, api_name="count") + return demo + + @pytest.fixture def file_io_demo(): demo = gr.Interface( diff --git a/demo/outbreak_forecast/requirements.txt b/demo/outbreak_forecast/requirements.txt index 5615a533fc386..7a0aa970fdc8f 100644 --- a/demo/outbreak_forecast/requirements.txt +++ b/demo/outbreak_forecast/requirements.txt @@ -2,4 +2,5 @@ numpy matplotlib bokeh plotly -altair \ No newline at end of file +altair +opencv-python \ No newline at end of file diff --git a/demo/outbreak_forecast/run.ipynb b/demo/outbreak_forecast/run.ipynb index 1ec9538ecdb55..1cfc4b9beaeae 100644 --- a/demo/outbreak_forecast/run.ipynb +++ b/demo/outbreak_forecast/run.ipynb @@ -1 +1 @@ -{"cells": [{"cell_type": "markdown", "id": "302934307671667531413257853548643485645", "metadata": {}, "source": ["# Gradio Demo: outbreak_forecast\n", "### Generate a plot based on 5 inputs.\n", " "]}, {"cell_type": "code", "execution_count": null, "id": "272996653310673477252411125948039410165", "metadata": {}, "outputs": [], "source": ["!pip install -q gradio numpy matplotlib bokeh plotly altair"]}, {"cell_type": "code", "execution_count": null, "id": "288918539441861185822528903084949547379", "metadata": {}, "outputs": [], "source": ["import altair\n", "\n", "import gradio as gr\n", "from math import sqrt\n", "import matplotlib.pyplot as plt\n", "import numpy as np\n", "import plotly.express as px\n", "import pandas as pd\n", "\n", "def outbreak(plot_type, r, month, countries, social_distancing):\n", " months = [\"January\", \"February\", \"March\", \"April\", \"May\"]\n", " m = months.index(month)\n", " start_day = 30 * m\n", " final_day = 30 * (m + 1)\n", " x = np.arange(start_day, final_day + 1)\n", " pop_count = {\"USA\": 350, \"Canada\": 40, \"Mexico\": 300, \"UK\": 120}\n", " if social_distancing:\n", " r = sqrt(r)\n", " df = pd.DataFrame({\"day\": x})\n", " for country in countries:\n", " df[country] = x ** (r) * (pop_count[country] + 1)\n", "\n", " if plot_type == \"Matplotlib\":\n", " fig = plt.figure()\n", " plt.plot(df[\"day\"], df[countries].to_numpy())\n", " plt.title(\"Outbreak in \" + month)\n", " plt.ylabel(\"Cases\")\n", " plt.xlabel(\"Days since Day 0\")\n", " plt.legend(countries)\n", " return fig\n", " elif plot_type == \"Plotly\":\n", " fig = px.line(df, x=\"day\", y=countries)\n", " fig.update_layout(\n", " title=\"Outbreak in \" + month,\n", " xaxis_title=\"Cases\",\n", " yaxis_title=\"Days Since Day 0\",\n", " )\n", " return fig\n", " elif plot_type == \"Altair\":\n", " df = df.melt(id_vars=\"day\").rename(columns={\"variable\": \"country\"})\n", " fig = altair.Chart(df).mark_line().encode(x=\"day\", y=\"value\", color=\"country\")\n", " return fig\n", " else:\n", " raise ValueError(\"A plot type must be selected\")\n", "\n", "inputs = [\n", " gr.Dropdown([\"Matplotlib\", \"Plotly\", \"Altair\"], label=\"Plot Type\"),\n", " gr.Slider(1, 4, 3.2, label=\"R\"),\n", " gr.Dropdown([\"January\", \"February\", \"March\", \"April\", \"May\"], label=\"Month\"),\n", " gr.CheckboxGroup(\n", " [\"USA\", \"Canada\", \"Mexico\", \"UK\"], label=\"Countries\", value=[\"USA\", \"Canada\"]\n", " ),\n", " gr.Checkbox(label=\"Social Distancing?\"),\n", "]\n", "outputs = gr.Plot()\n", "\n", "demo = gr.Interface(\n", " fn=outbreak,\n", " inputs=inputs,\n", " outputs=outputs,\n", " examples=[\n", " [\"Matplotlib\", 2, \"March\", [\"Mexico\", \"UK\"], True],\n", " [\"Altair\", 2, \"March\", [\"Mexico\", \"Canada\"], True],\n", " [\"Plotly\", 3.6, \"February\", [\"Canada\", \"Mexico\", \"UK\"], False],\n", " ],\n", " cache_examples=True,\n", ")\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: outbreak_forecast\n", "### Generate a plot based on 5 inputs.\n", " "]}, {"cell_type": "code", "execution_count": null, "id": "272996653310673477252411125948039410165", "metadata": {}, "outputs": [], "source": ["!pip install -q gradio numpy matplotlib bokeh plotly altair opencv-python"]}, {"cell_type": "code", "execution_count": null, "id": "288918539441861185822528903084949547379", "metadata": {}, "outputs": [], "source": ["import altair\n", "\n", "import gradio as gr\n", "from math import sqrt\n", "import matplotlib.pyplot as plt\n", "import numpy as np\n", "import plotly.express as px\n", "import pandas as pd\n", "\n", "def outbreak(plot_type, r, month, countries, social_distancing):\n", " months = [\"January\", \"February\", \"March\", \"April\", \"May\"]\n", " m = months.index(month)\n", " start_day = 30 * m\n", " final_day = 30 * (m + 1)\n", " x = np.arange(start_day, final_day + 1)\n", " pop_count = {\"USA\": 350, \"Canada\": 40, \"Mexico\": 300, \"UK\": 120}\n", " if social_distancing:\n", " r = sqrt(r)\n", " df = pd.DataFrame({\"day\": x})\n", " for country in countries:\n", " df[country] = x ** (r) * (pop_count[country] + 1)\n", "\n", " if plot_type == \"Matplotlib\":\n", " fig = plt.figure()\n", " plt.plot(df[\"day\"], df[countries].to_numpy())\n", " plt.title(\"Outbreak in \" + month)\n", " plt.ylabel(\"Cases\")\n", " plt.xlabel(\"Days since Day 0\")\n", " plt.legend(countries)\n", " return fig\n", " elif plot_type == \"Plotly\":\n", " fig = px.line(df, x=\"day\", y=countries)\n", " fig.update_layout(\n", " title=\"Outbreak in \" + month,\n", " xaxis_title=\"Cases\",\n", " yaxis_title=\"Days Since Day 0\",\n", " )\n", " return fig\n", " elif plot_type == \"Altair\":\n", " df = df.melt(id_vars=\"day\").rename(columns={\"variable\": \"country\"})\n", " fig = altair.Chart(df).mark_line().encode(x=\"day\", y=\"value\", color=\"country\")\n", " return fig\n", " else:\n", " raise ValueError(\"A plot type must be selected\")\n", "\n", "inputs = [\n", " gr.Dropdown([\"Matplotlib\", \"Plotly\", \"Altair\"], label=\"Plot Type\"),\n", " gr.Slider(1, 4, 3.2, label=\"R\"),\n", " gr.Dropdown([\"January\", \"February\", \"March\", \"April\", \"May\"], label=\"Month\"),\n", " gr.CheckboxGroup(\n", " [\"USA\", \"Canada\", \"Mexico\", \"UK\"], label=\"Countries\", value=[\"USA\", \"Canada\"]\n", " ),\n", " gr.Checkbox(label=\"Social Distancing?\"),\n", "]\n", "outputs = gr.Plot()\n", "\n", "demo = gr.Interface(\n", " fn=outbreak,\n", " inputs=inputs,\n", " outputs=outputs,\n", " examples=[\n", " [\"Matplotlib\", 2, \"March\", [\"Mexico\", \"UK\"], True],\n", " [\"Altair\", 2, \"March\", [\"Mexico\", \"Canada\"], True],\n", " [\"Plotly\", 3.6, \"February\", [\"Canada\", \"Mexico\", \"UK\"], False],\n", " ],\n", " cache_examples=True,\n", ")\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/stream_audio_out/run.ipynb b/demo/stream_audio_out/run.ipynb index 94765656a34f7..a1a746709c850 100644 --- a/demo/stream_audio_out/run.ipynb +++ b/demo/stream_audio_out/run.ipynb @@ -1 +1 @@ -{"cells": [{"cell_type": "markdown", "id": "302934307671667531413257853548643485645", "metadata": {}, "source": ["# Gradio Demo: stream_audio_out"]}, {"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": ["# Downloading files from the demo repo\n", "import os\n", "os.mkdir('audio')\n", "!wget -q -O audio/cantina.wav https://github.com/gradio-app/gradio/raw/main/demo/stream_audio_out/audio/cantina.wav"]}, {"cell_type": "code", "execution_count": null, "id": "44380577570523278879349135829904343037", "metadata": {}, "outputs": [], "source": ["import gradio as gr\n", "from pydub import AudioSegment\n", "from time import sleep\n", "\n", "with gr.Blocks() as demo:\n", " input_audio = gr.Audio(label=\"Input Audio\", type=\"filepath\", format=\"mp3\")\n", " with gr.Row():\n", " with gr.Column():\n", " stream_as_file_btn = gr.Button(\"Stream as File\")\n", " format = gr.Radio([\"wav\", \"mp3\"], value=\"wav\", label=\"Format\")\n", " stream_as_file_output = gr.Audio(streaming=True)\n", "\n", " def stream_file(audio_file, format):\n", " audio = AudioSegment.from_file(audio_file)\n", " i = 0\n", " chunk_size = 1000\n", " while chunk_size * i < len(audio):\n", " chunk = audio[chunk_size * i : chunk_size * (i + 1)]\n", " i += 1\n", " if chunk:\n", " file = f\"/tmp/{i}.{format}\"\n", " chunk.export(file, format=format)\n", " yield file\n", " sleep(0.5)\n", "\n", " stream_as_file_btn.click(\n", " stream_file, [input_audio, format], stream_as_file_output\n", " )\n", "\n", " gr.Examples(\n", " [[\"audio/cantina.wav\", \"wav\"], [\"audio/cantina.wav\", \"mp3\"]],\n", " [input_audio, format],\n", " fn=stream_file,\n", " outputs=stream_as_file_output,\n", " )\n", "\n", " with gr.Column():\n", " stream_as_bytes_btn = gr.Button(\"Stream as Bytes\")\n", " stream_as_bytes_output = gr.Audio(streaming=True)\n", "\n", " def stream_bytes(audio_file):\n", " chunk_size = 20_000\n", " with open(audio_file, \"rb\") as f:\n", " while True:\n", " chunk = f.read(chunk_size)\n", " if chunk:\n", " yield chunk\n", " sleep(1)\n", " else:\n", " break\n", " stream_as_bytes_btn.click(stream_bytes, input_audio, stream_as_bytes_output)\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: stream_audio_out"]}, {"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": ["# Downloading files from the demo repo\n", "import os\n", "os.mkdir('audio')\n", "!wget -q -O audio/cantina.wav https://github.com/gradio-app/gradio/raw/main/demo/stream_audio_out/audio/cantina.wav"]}, {"cell_type": "code", "execution_count": null, "id": "44380577570523278879349135829904343037", "metadata": {}, "outputs": [], "source": ["import gradio as gr\n", "from pydub import AudioSegment\n", "from time import sleep\n", "import os\n", "\n", "with gr.Blocks() as demo:\n", " input_audio = gr.Audio(label=\"Input Audio\", type=\"filepath\", format=\"mp3\")\n", " with gr.Row():\n", " with gr.Column():\n", " stream_as_file_btn = gr.Button(\"Stream as File\")\n", " format = gr.Radio([\"wav\", \"mp3\"], value=\"wav\", label=\"Format\")\n", " stream_as_file_output = gr.Audio(streaming=True, elem_id=\"stream_as_file_output\", autoplay=True)\n", "\n", " def stream_file(audio_file, format):\n", " audio = AudioSegment.from_file(audio_file)\n", " i = 0\n", " chunk_size = 1000\n", " while chunk_size * i < len(audio):\n", " chunk = audio[chunk_size * i : chunk_size * (i + 1)]\n", " i += 1\n", " if chunk:\n", " file = f\"/tmp/{i}.{format}\"\n", " chunk.export(file, format=format)\n", " yield file\n", " sleep(0.5)\n", "\n", " stream_as_file_btn.click(\n", " stream_file, [input_audio, format], stream_as_file_output\n", " )\n", "\n", " gr.Examples(\n", " [[os.path.join(os.path.abspath(''), \"audio/cantina.wav\"), \"wav\"],\n", " [os.path.join(os.path.abspath(''), \"audio/cantina.wav\"), \"mp3\"]],\n", " [input_audio, format],\n", " fn=stream_file,\n", " outputs=stream_as_file_output,\n", " cache_examples=False,\n", " )\n", "\n", " with gr.Column():\n", " stream_as_bytes_btn = gr.Button(\"Stream as Bytes\")\n", " stream_as_bytes_output = gr.Audio(streaming=True, elem_id=\"stream_as_bytes_output\", autoplay=True)\n", "\n", " def stream_bytes(audio_file):\n", " chunk_size = 20_000\n", " with open(audio_file, \"rb\") as f:\n", " while True:\n", " chunk = f.read(chunk_size)\n", " if chunk:\n", " yield chunk\n", " sleep(1)\n", " else:\n", " break\n", " stream_as_bytes_btn.click(stream_bytes, input_audio, stream_as_bytes_output)\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/stream_audio_out/run.py b/demo/stream_audio_out/run.py index 9b348c532fcb6..32e09fcc9ad53 100644 --- a/demo/stream_audio_out/run.py +++ b/demo/stream_audio_out/run.py @@ -1,6 +1,7 @@ import gradio as gr from pydub import AudioSegment from time import sleep +import os with gr.Blocks() as demo: input_audio = gr.Audio(label="Input Audio", type="filepath", format="mp3") @@ -8,7 +9,7 @@ with gr.Column(): stream_as_file_btn = gr.Button("Stream as File") format = gr.Radio(["wav", "mp3"], value="wav", label="Format") - stream_as_file_output = gr.Audio(streaming=True) + stream_as_file_output = gr.Audio(streaming=True, elem_id="stream_as_file_output", autoplay=True) def stream_file(audio_file, format): audio = AudioSegment.from_file(audio_file) @@ -28,15 +29,17 @@ def stream_file(audio_file, format): ) gr.Examples( - [["audio/cantina.wav", "wav"], ["audio/cantina.wav", "mp3"]], + [[os.path.join(os.path.dirname(__file__), "audio/cantina.wav"), "wav"], + [os.path.join(os.path.dirname(__file__), "audio/cantina.wav"), "mp3"]], [input_audio, format], fn=stream_file, outputs=stream_as_file_output, + cache_examples=False, ) with gr.Column(): stream_as_bytes_btn = gr.Button("Stream as Bytes") - stream_as_bytes_output = gr.Audio(streaming=True) + stream_as_bytes_output = gr.Audio(streaming=True, elem_id="stream_as_bytes_output", autoplay=True) def stream_bytes(audio_file): chunk_size = 20_000 diff --git a/demo/stream_video_out/run.ipynb b/demo/stream_video_out/run.ipynb new file mode 100644 index 0000000000000..5200bdb2409b4 --- /dev/null +++ b/demo/stream_video_out/run.ipynb @@ -0,0 +1 @@ +{"cells": [{"cell_type": "markdown", "id": "302934307671667531413257853548643485645", "metadata": {}, "source": ["# Gradio Demo: stream_video_out"]}, {"cell_type": "code", "execution_count": null, "id": "272996653310673477252411125948039410165", "metadata": {}, "outputs": [], "source": ["!pip install -q gradio opencv-python"]}, {"cell_type": "code", "execution_count": null, "id": "288918539441861185822528903084949547379", "metadata": {}, "outputs": [], "source": ["# Downloading files from the demo repo\n", "import os\n", "os.mkdir('video')\n", "!wget -q -O video/compliment_bot_screen_recording_3x.mp4 https://github.com/gradio-app/gradio/raw/main/demo/stream_video_out/video/compliment_bot_screen_recording_3x.mp4"]}, {"cell_type": "code", "execution_count": null, "id": "44380577570523278879349135829904343037", "metadata": {}, "outputs": [], "source": ["import gradio as gr\n", "import cv2\n", "import os\n", "from pathlib import Path\n", "import atexit\n", "\n", "current_dir = Path(__file__).resolve().parent\n", "\n", "\n", "def delete_files():\n", " for p in Path(current_dir).glob(\"*.ts\"):\n", " p.unlink()\n", " for p in Path(current_dir).glob(\"*.mp4\"):\n", " p.unlink()\n", "\n", "atexit.register(delete_files)\n", "\n", "\n", "def process_video(input_video, stream_as_mp4):\n", " cap = cv2.VideoCapture(input_video)\n", "\n", " video_codec = cv2.VideoWriter_fourcc(*\"mp4v\") if stream_as_mp4 else cv2.VideoWriter_fourcc(*\"x264\") # type: ignore\n", " fps = int(cap.get(cv2.CAP_PROP_FPS))\n", " width = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH))\n", " height = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT))\n", "\n", " iterating, frame = cap.read()\n", "\n", " n_frames = 0\n", " n_chunks = 0\n", " name = str(current_dir / f\"output_{n_chunks}{'.mp4' if stream_as_mp4 else '.ts'}\")\n", " segment_file = cv2.VideoWriter(name, video_codec, fps, (width, height)) # type: ignore\n", "\n", " while iterating:\n", "\n", " # flip frame vertically\n", " frame = cv2.flip(frame, 0)\n", " display_frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)\n", " segment_file.write(display_frame)\n", " n_frames += 1\n", " if n_frames == 3 * fps:\n", " n_chunks += 1\n", " segment_file.release()\n", " n_frames = 0\n", " yield name\n", " name = str(current_dir / f\"output_{n_chunks}{'.mp4' if stream_as_mp4 else '.ts'}\")\n", " segment_file = cv2.VideoWriter(name, video_codec, fps, (width, height)) # type: ignore\n", "\n", " iterating, frame = cap.read()\n", "\n", " segment_file.release()\n", " yield name\n", "\n", "with gr.Blocks() as demo:\n", " gr.Markdown(\"# Video Streaming Out \ud83d\udcf9\")\n", " with gr.Row():\n", " with gr.Column():\n", " input_video = gr.Video(label=\"input\")\n", " checkbox = gr.Checkbox(label=\"Stream as MP4 file?\", value=False)\n", " with gr.Column():\n", " processed_frames = gr.Video(label=\"stream\", streaming=True, autoplay=True, elem_id=\"stream_video_output\")\n", " with gr.Row():\n", " process_video_btn = gr.Button(\"process video\")\n", "\n", " process_video_btn.click(process_video, [input_video, checkbox], [processed_frames])\n", "\n", " gr.Examples(\n", " [[os.path.join(os.path.abspath(''), \"video/compliment_bot_screen_recording_3x.mp4\"), False],\n", " [os.path.join(os.path.abspath(''), \"video/compliment_bot_screen_recording_3x.mp4\"), True]],\n", " [input_video, checkbox],\n", " fn=process_video,\n", " outputs=processed_frames,\n", " cache_examples=False,\n", " )\n", "\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/stream_video_out/run.py b/demo/stream_video_out/run.py new file mode 100644 index 0000000000000..94999f6593454 --- /dev/null +++ b/demo/stream_video_out/run.py @@ -0,0 +1,78 @@ +import gradio as gr +import cv2 +import os +from pathlib import Path +import atexit + +current_dir = Path(__file__).resolve().parent + + +def delete_files(): + for p in Path(current_dir).glob("*.ts"): + p.unlink() + for p in Path(current_dir).glob("*.mp4"): + p.unlink() + +atexit.register(delete_files) + + +def process_video(input_video, stream_as_mp4): + cap = cv2.VideoCapture(input_video) + + video_codec = cv2.VideoWriter_fourcc(*"mp4v") if stream_as_mp4 else cv2.VideoWriter_fourcc(*"x264") # type: ignore + fps = int(cap.get(cv2.CAP_PROP_FPS)) + width = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH)) + height = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT)) + + iterating, frame = cap.read() + + n_frames = 0 + n_chunks = 0 + name = str(current_dir / f"output_{n_chunks}{'.mp4' if stream_as_mp4 else '.ts'}") + segment_file = cv2.VideoWriter(name, video_codec, fps, (width, height)) # type: ignore + + while iterating: + + # flip frame vertically + frame = cv2.flip(frame, 0) + display_frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) + segment_file.write(display_frame) + n_frames += 1 + if n_frames == 3 * fps: + n_chunks += 1 + segment_file.release() + n_frames = 0 + yield name + name = str(current_dir / f"output_{n_chunks}{'.mp4' if stream_as_mp4 else '.ts'}") + segment_file = cv2.VideoWriter(name, video_codec, fps, (width, height)) # type: ignore + + iterating, frame = cap.read() + + segment_file.release() + yield name + +with gr.Blocks() as demo: + gr.Markdown("# Video Streaming Out 📹") + with gr.Row(): + with gr.Column(): + input_video = gr.Video(label="input") + checkbox = gr.Checkbox(label="Stream as MP4 file?", value=False) + with gr.Column(): + processed_frames = gr.Video(label="stream", streaming=True, autoplay=True, elem_id="stream_video_output") + with gr.Row(): + process_video_btn = gr.Button("process video") + + process_video_btn.click(process_video, [input_video, checkbox], [processed_frames]) + + gr.Examples( + [[os.path.join(os.path.dirname(__file__), "video/compliment_bot_screen_recording_3x.mp4"), False], + [os.path.join(os.path.dirname(__file__), "video/compliment_bot_screen_recording_3x.mp4"), True]], + [input_video, checkbox], + fn=process_video, + outputs=processed_frames, + cache_examples=False, + ) + + +if __name__ == "__main__": + demo.launch() diff --git a/demo/stream_video_out/video/compliment_bot_screen_recording_3x.mp4 b/demo/stream_video_out/video/compliment_bot_screen_recording_3x.mp4 new file mode 100644 index 0000000000000000000000000000000000000000..7a7395bf43b404b3ff9ab91995ccda38eafab334 GIT binary patch literal 222882 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⩽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 ZfwWboGMWm_ list: if session_hash is None or run is None: return data if run not in self.pending_streams[session_hash]: self.pending_streams[session_hash][run] = {} - stream_run = self.pending_streams[session_hash][run] + stream_run: dict[int, MediaStream] = self.pending_streams[session_hash][run] for i, block in enumerate(block_fn.outputs): output_id = block._id if isinstance(block, components.StreamingOutput) and block.streaming: + if final: + stream_run[output_id].end_stream() first_chunk = output_id not in stream_run - binary_data, output_data = block.stream_output( - data[i], f"{session_hash}/{run}/{output_id}", first_chunk + binary_data, output_data = await block.stream_output( + data[i], + f"{session_hash}/{run}/{output_id}/playlist.m3u8", + first_chunk, ) if first_chunk: - stream_run[output_id] = [] - self.pending_streams[session_hash][run][output_id].append(binary_data) + stream_run[output_id] = MediaStream() + + await stream_run[output_id].add_segment(binary_data) output_data = await processing_utils.async_move_files_to_cache( output_data, block, diff --git a/gradio/components/audio.py b/gradio/components/audio.py index e963847ec1e02..e28dfcfa929cf 100644 --- a/gradio/components/audio.py +++ b/gradio/components/audio.py @@ -3,18 +3,21 @@ from __future__ import annotations import dataclasses +import io from pathlib import Path from typing import TYPE_CHECKING, Any, Callable, Literal, Sequence +import anyio import httpx import numpy as np from gradio_client import handle_file from gradio_client import utils as client_utils from gradio_client.documentation import document +from pydub import AudioSegment from gradio import processing_utils, utils from gradio.components.base import Component, StreamingInput, StreamingOutput -from gradio.data_classes import FileData +from gradio.data_classes import FileData, FileDataDict, MediaStreamChunk from gradio.events import Events from gradio.exceptions import Error @@ -287,38 +290,49 @@ def postprocess( orig_name = Path(file_path).name if Path(file_path).exists() else None return FileData(path=file_path, orig_name=orig_name) - def stream_output( - self, value, output_id: str, first_chunk: bool - ) -> tuple[bytes | None, Any]: - output_file = { + @staticmethod + def _convert_to_adts(data: bytes): + segment = AudioSegment.from_file(io.BytesIO(data)) + + buffer = io.BytesIO() + segment.export(buffer, format="adts") # ADTS is a container format for AAC + aac_data = buffer.getvalue() + return aac_data, len(segment) / 1000.0 + + @staticmethod + async def covert_to_adts(data: bytes) -> tuple[bytes, float]: + return await anyio.to_thread.run_sync(Audio._convert_to_adts, data) + + async def stream_output( + self, + value, + output_id: str, + first_chunk: bool, # noqa: ARG002 + ) -> tuple[MediaStreamChunk | None, FileDataDict]: + output_file: FileDataDict = { "path": output_id, "is_stream": True, + "orig_name": "audio-stream.mp3", } if value is None: return None, output_file if isinstance(value, bytes): - return value, output_file + value, duration = await self.covert_to_adts(value) + return { + "data": value, + "duration": duration, + "extension": ".aac", + }, output_file if client_utils.is_http_url_like(value["path"]): response = httpx.get(value["path"]) binary_data = response.content else: output_file["orig_name"] = value["orig_name"] file_path = value["path"] - is_wav = file_path.endswith(".wav") with open(file_path, "rb") as f: binary_data = f.read() - if is_wav: - # strip length information from first chunk header, remove headers entirely from subsequent chunks - if first_chunk: - binary_data = ( - binary_data[:4] + b"\xff\xff\xff\xff" + binary_data[8:] - ) - binary_data = ( - binary_data[:40] + b"\xff\xff\xff\xff" + binary_data[44:] - ) - else: - binary_data = binary_data[44:] - return binary_data, output_file + value, duration = await self.covert_to_adts(binary_data) + return {"data": value, "duration": duration, "extension": ".aac"}, output_file def process_example( self, value: tuple[int, np.ndarray] | str | Path | bytes | None diff --git a/gradio/components/base.py b/gradio/components/base.py index 93efa94f699b0..7324468d0b21c 100644 --- a/gradio/components/base.py +++ b/gradio/components/base.py @@ -19,7 +19,12 @@ from gradio import utils from gradio.blocks import Block, BlockContext from gradio.component_meta import ComponentMeta -from gradio.data_classes import BaseModel, GradioDataModel +from gradio.data_classes import ( + BaseModel, + FileDataDict, + GradioDataModel, + MediaStreamChunk, +) from gradio.events import EventListener from gradio.layouts import Form from gradio.processing_utils import move_files_to_cache @@ -371,9 +376,9 @@ def __init__(self, *args, **kwargs) -> None: self.streaming: bool @abc.abstractmethod - def stream_output( + async def stream_output( self, value, output_id: str, first_chunk: bool - ) -> tuple[bytes | None, Any]: + ) -> tuple[MediaStreamChunk | None, FileDataDict | dict]: pass diff --git a/gradio/components/video.py b/gradio/components/video.py index 7e03a7440c1d5..37ff7b535925e 100644 --- a/gradio/components/video.py +++ b/gradio/components/video.py @@ -2,6 +2,9 @@ from __future__ import annotations +import asyncio +import json +import subprocess import tempfile import warnings from pathlib import Path @@ -13,8 +16,8 @@ import gradio as gr from gradio import processing_utils, utils, wasm_utils -from gradio.components.base import Component -from gradio.data_classes import FileData, GradioModel +from gradio.components.base import Component, StreamingOutput +from gradio.data_classes import FileData, GradioModel, MediaStreamChunk from gradio.events import Events if TYPE_CHECKING: @@ -31,7 +34,7 @@ class VideoData(GradioModel): @document() -class Video(Component): +class Video(StreamingOutput, Component): """ Creates a video component that can be used to upload/record videos (as an input) or display videos (as an output). For the video to be playable in the browser it must have a compatible container and codec combination. Allowed @@ -91,6 +94,7 @@ def __init__( min_length: int | None = None, max_length: int | None = None, loop: bool = False, + streaming: bool = False, watermark: str | Path | None = None, ): """ @@ -121,6 +125,7 @@ def __init__( min_length: The minimum length of video (in seconds) that the user can pass into the prediction function. If None, there is no minimum length. max_length: The maximum length of video (in seconds) that the user can pass into the prediction function. If None, there is no maximum length. loop: If True, the video will loop when it reaches the end and continue playing from the beginning. + streaming: When used set as an output, takes video chunks yielded from the backend and combines them into one streaming video output. Each chunk should be a video file with a .ts extension using an h.264 encoding. Mp4 files are also accepted but they will be converted to h.264 encoding. watermark: An image file to be included as a watermark on the video. The image is not scaled and is displayed on the bottom right of the video. Valid formats for the image are: jpeg, png. """ valid_sources: list[Literal["upload", "webcam"]] = ["upload", "webcam"] @@ -156,6 +161,7 @@ def __init__( self.show_download_button = show_download_button self.min_length = min_length self.max_length = max_length + self.streaming = streaming self.watermark = watermark super().__init__( label=label, @@ -263,6 +269,8 @@ def postprocess( Returns: VideoData object containing the video and subtitle files. """ + if self.streaming: + return value # type: ignore if value is None or value == [None, None] or value == (None, None): return None if isinstance(value, (str, Path)): @@ -411,3 +419,91 @@ def example_payload(self) -> Any: def example_value(self) -> Any: return "https://github.com/gradio-app/gradio/raw/main/demo/video_component/files/world.mp4" + + @staticmethod + def get_video_duration_ffprobe(filename: str): + result = subprocess.run( + [ + "ffprobe", + "-v", + "quiet", + "-print_format", + "json", + "-show_format", + "-show_streams", + filename, + ], + capture_output=True, + check=True, + ) + + data = json.loads(result.stdout) + + duration = None + if "format" in data and "duration" in data["format"]: + duration = float(data["format"]["duration"]) + else: + for stream in data.get("streams", []): + if "duration" in stream: + duration = float(stream["duration"]) + break + + return duration + + @staticmethod + async def async_convert_mp4_to_ts(mp4_file, ts_file): + ff = FFmpeg( # type: ignore + inputs={mp4_file: None}, + outputs={ + ts_file: "-c:v libx264 -c:a aac -f mpegts -bsf:v h264_mp4toannexb -bsf:a aac_adtstoasc" + }, + global_options=["-y"], + ) + + command = ff.cmd.split(" ") + process = await asyncio.create_subprocess_exec( + *command, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE + ) + + _, stderr = await process.communicate() + + if process.returncode != 0: + error_message = stderr.decode().strip() + raise RuntimeError(f"FFmpeg command failed: {error_message}") + + return ts_file + + async def stream_output( + self, + value: str | None, + output_id: str, + first_chunk: bool, # noqa: ARG002 + ) -> tuple[MediaStreamChunk | None, dict]: + output_file = { + "video": { + "path": output_id, + "is_stream": True, + "orig_name": "video-stream.ts", + } + } + if value is None: + return None, output_file + + ts_file = value + if not value.endswith(".ts"): + if not value.endswith(".mp4"): + raise RuntimeError( + "Video must be in .mp4 or .ts format to be streamed as chunks", + ) + ts_file = value.replace(".mp4", ".ts") + await self.async_convert_mp4_to_ts(value, ts_file) + + duration = self.get_video_duration_ffprobe(ts_file) + if not duration: + raise RuntimeError("Cannot determine video chunk duration") + chunk: MediaStreamChunk = { + "data": Path(ts_file).read_bytes(), + "duration": duration, + "extension": ".ts", + } + return chunk, output_file diff --git a/gradio/data_classes.py b/gradio/data_classes.py index 7c1489d2b1756..26e5c25c81c07 100644 --- a/gradio/data_classes.py +++ b/gradio/data_classes.py @@ -150,12 +150,12 @@ def from_json(cls, x) -> GradioRootModel: class FileDataDict(TypedDict): path: str # server filepath - url: Optional[str] # normalised server url - size: Optional[int] # size in bytes - orig_name: Optional[str] # original filename - mime_type: Optional[str] + url: NotRequired[Optional[str]] # normalised server url + size: NotRequired[Optional[int]] # size in bytes + orig_name: NotRequired[Optional[str]] # original filename + mime_type: NotRequired[Optional[str]] is_stream: bool - meta: dict + meta: NotRequired[dict] @document() @@ -321,3 +321,10 @@ class BlocksConfigDict(TypedDict): dependencies: NotRequired[list[dict[str, Any]]] root: NotRequired[str | None] username: NotRequired[str | None] + + +class MediaStreamChunk(TypedDict): + data: bytes + duration: float + extension: str + id: NotRequired[str] diff --git a/gradio/helpers.py b/gradio/helpers.py index 0dd31bf5784f0..9039838423a06 100644 --- a/gradio/helpers.py +++ b/gradio/helpers.py @@ -521,7 +521,7 @@ async def get_final_item(*args): ) output = prediction["data"] if len(generated_values): - output = merge_generated_values_into_output( + output = await merge_generated_values_into_output( self.outputs, generated_values, output ) if self.batch: @@ -583,7 +583,7 @@ def load_from_cache(self, example_id: int) -> list[Any]: return output -def merge_generated_values_into_output( +async def merge_generated_values_into_output( components: Sequence[Component], generated_values: list, output: list ): from gradio.components.base import StreamingOutput @@ -598,9 +598,11 @@ def merge_generated_values_into_output( if isinstance(processed_chunk, (GradioModel, GradioRootModel)): processed_chunk = processed_chunk.model_dump() binary_chunks.append( - output_component.stream_output(processed_chunk, "", i == 0)[0] + (await output_component.stream_output(processed_chunk, "", i == 0))[ + 0 + ] ) - binary_data = b"".join(binary_chunks) + binary_data = b"".join([d["data"] for d in binary_chunks]) tempdir = os.environ.get("GRADIO_TEMP_DIR") or str( Path(tempfile.gettempdir()) / "gradio" ) diff --git a/gradio/route_utils.py b/gradio/route_utils.py index f6e6e64c16177..89be75bdde40b 100644 --- a/gradio/route_utils.py +++ b/gradio/route_utils.py @@ -10,6 +10,7 @@ import shutil import sys import threading +import uuid from collections import deque from contextlib import AsyncExitStack, asynccontextmanager from dataclasses import dataclass as python_dataclass @@ -42,7 +43,7 @@ from starlette.types import ASGIApp, Message, Receive, Scope, Send from gradio import processing_utils, utils -from gradio.data_classes import BlocksConfigDict, PredictBody +from gradio.data_classes import BlocksConfigDict, MediaStreamChunk, PredictBody from gradio.exceptions import Error from gradio.helpers import EventData from gradio.state_holder import SessionState @@ -304,11 +305,11 @@ async def call_process_api( iterator = app.iterators.get(event_id) if event_id is not None else None if iterator is not None: # close off any streams that are still open run_id = id(iterator) - pending_streams: dict[int, list] = ( + pending_streams: dict[int, MediaStream] = ( app.get_blocks().pending_streams[session_hash].get(run_id, {}) ) for stream in pending_streams.values(): - stream.append(None) + stream.end_stream() raise if batch_in_single_out: @@ -854,3 +855,21 @@ async def _handler(app: App): yield return _handler + + +class MediaStream: + def __init__(self): + self.segments: list[MediaStreamChunk] = [] + self.ended = False + self.segment_index = 0 + self.playlist = "#EXTM3U\n#EXT-X-PLAYLIST-TYPE:EVENT\n#EXT-X-TARGETDURATION:10\n#EXT-X-VERSION:4\n#EXT-X-MEDIA-SEQUENCE:0\n" + + async def add_segment(self, data: MediaStreamChunk | None): + if not data: + return + + segment_id = str(uuid.uuid4()) + self.segments.append({"id": segment_id, **data}) + + def end_stream(self): + self.ended = True diff --git a/gradio/routes.py b/gradio/routes.py index 544a99b62f7e6..1275c220cdb94 100644 --- a/gradio/routes.py +++ b/gradio/routes.py @@ -53,6 +53,7 @@ HTMLResponse, JSONResponse, PlainTextResponse, + Response, ) from fastapi.security import OAuth2PasswordRequestForm from fastapi.templating import Jinja2Templates @@ -589,43 +590,78 @@ async def file(path_or_url: str, request: fastapi.Request): return FileResponse(abs_path, headers={"Accept-Ranges": "bytes"}) - @app.get( - "/stream/{session_hash}/{run}/{component_id}", - dependencies=[Depends(login_check)], - ) - async def stream( - session_hash: str, - run: int, - component_id: int, - request: fastapi.Request, # noqa: ARG001 + @app.get("/stream/{session_hash}/{run}/{component_id}/playlist.m3u8") + async def _(session_hash: str, run: int, component_id: int): + stream: route_utils.MediaStream | None = ( + app.get_blocks() + .pending_streams[session_hash] + .get(run, {}) + .get(component_id, None) + ) + + if not stream: + return Response(status_code=404) + + playlist = "#EXTM3U\n#EXT-X-PLAYLIST-TYPE:EVENT\n#EXT-X-TARGETDURATION:3\n#EXT-X-VERSION:4\n#EXT-X-MEDIA-SEQUENCE:0\n" + + for segment in stream.segments: + playlist += f"#EXTINF:{segment['duration']:.3f},\n" + playlist += f"{segment['id']}{segment['extension']}\n" # type: ignore + + if stream.ended: + playlist += "#EXT-X-ENDLIST\n" + + return Response( + content=playlist, media_type="application/vnd.apple.mpegurl" + ) + + @app.get("/stream/{session_hash}/{run}/{component_id}/{segment_id}.{ext}") + async def _( + session_hash: str, run: int, component_id: int, segment_id: str, ext: str ): - stream: list = ( + if ext not in ["aac", "ts"]: + return Response(status_code=400, content="Unsupported file extension") + stream: route_utils.MediaStream | None = ( app.get_blocks() .pending_streams[session_hash] .get(run, {}) .get(component_id, None) ) - if stream is None: - raise HTTPException(404, "Stream not found.") - def stream_wrapper(): - check_stream_rate = 0.01 - max_wait_time = 120 # maximum wait between yields - assume generator thread has crashed otherwise. - wait_time = 0 - while True: - if len(stream) == 0: - if wait_time > max_wait_time: - return - wait_time += check_stream_rate - time.sleep(check_stream_rate) - continue - wait_time = 0 - next_stream = stream.pop(0) - if next_stream is None: - return - yield next_stream + if not stream: + return Response(status_code=404, content="Stream not found") + + segment = next((s for s in stream.segments if s["id"] == segment_id), None) # type: ignore + + if segment is None: + return Response(status_code=404, content="Segment not found") + + if ext == "aac": + return Response(content=segment["data"], media_type="audio/aac") + else: + return Response(content=segment["data"], media_type="video/MP2T") + + @app.get("/stream/{session_hash}/{run}/{component_id}/playlist-file") + async def _(session_hash: str, run: int, component_id: int): + stream: route_utils.MediaStream | None = ( + app.get_blocks() + .pending_streams[session_hash] + .get(run, {}) + .get(component_id, None) + ) + + if not stream: + return Response(status_code=404) + + byte_stream = b"" + extension = "" + for segment in stream.segments: + extension = segment["extension"] + byte_stream += segment["data"] + + media_type = "video/MP2T" if extension == ".ts" else "audio/aac" - return StreamingResponse(stream_wrapper()) + return Response(content=byte_stream, media_type=media_type) @app.get("/file/{path:path}", dependencies=[Depends(login_check)]) async def file_deprecated(path: str, request: fastapi.Request): diff --git a/gradio/templates.py b/gradio/templates.py index ba9ed56f0f812..e9fad9299e08c 100644 --- a/gradio/templates.py +++ b/gradio/templates.py @@ -371,6 +371,7 @@ def __init__( min_length: int | None = None, max_length: int | None = None, loop: bool = False, + streaming: bool = False, watermark: str | Path | None = None, ): sources = ["upload"] @@ -401,6 +402,7 @@ def __init__( min_length=min_length, max_length=max_length, loop=loop, + streaming=streaming, watermark=watermark, ) diff --git a/guides/04_additional-features/02_streaming-outputs.md b/guides/04_additional-features/02_streaming-outputs.md index b3d41009052ed..57e3bd1064b58 100644 --- a/guides/04_additional-features/02_streaming-outputs.md +++ b/guides/04_additional-features/02_streaming-outputs.md @@ -18,3 +18,53 @@ $demo_fake_diffusion Note that we've added a `time.sleep(1)` in the iterator to create an artificial pause between steps so that you are able to observe the steps of the iterator (in a real image generation model, this probably wouldn't be necessary). Similarly, Gradio can handle streaming inputs, e.g. an image generation model that reruns every time a user types a letter in a textbox. This is covered in more details in our guide on building [reactive Interfaces](/guides/reactive-interfaces). + +## Streaming Media + +Gradio can stream audio and video directly from your generator function. +This lets your user hear your audio or see your video nearly as soon as it's `yielded` by your function. +All you have to do is + +1. Set `streaming=True` in your `gr.Audio` or `gr.Video` output component. +2. Write a python generator that yields the next "chunk" of audio or video. +3. Set `autoplay=True` so that the media starts playing automatically. + +For audio, the next "chunk" can be either an `.mp3` or `.wav` file or a `bytes` sequence of audio. +For video, the next "chunk" has to be either `.mp4` file or a file with `h.264` codec with a `.ts` extension. +For smooth playback, make sure chunks are consistent lengths and larger than 1 second. + +Let's look at some examples. + +### Streaming Audio + +```python +import gradio as gr +from time import sleep + +def keep_repeating(audio_file): + for _ in range(10): + sleep(0.5) + yield audio_file + +gr.Interface(keep_repeating, + gr.Audio(sources=["microphone"], type="filepath"), + gr.Audio(streaming=True, autoplay=True) +).launch() +``` + +### Streaming Video + +```python +import gradio as gr +from time import sleep + +def keep_repeating(video_file): + for _ in range(10): + sleep(0.5) + yield video_file + +gr.Interface(keep_repeating, + gr.Video(sources=["webcam"], format="mp4"), + gr.Video(streaming=True, autoplay=True) +).launch() +``` \ No newline at end of file diff --git a/js/app/test/blocks_essay.spec.ts b/js/app/test/blocks_essay.spec.ts index 036c392eae288..1dc3ded0d6337 100644 --- a/js/app/test/blocks_essay.spec.ts +++ b/js/app/test/blocks_essay.spec.ts @@ -54,15 +54,12 @@ test("updates backend correctly", async ({ page }) => { test("updates dropdown choices correctly", async ({ page }) => { const country = await page.getByLabel("Country").first(); const city = await page.getByLabel("Cities").first(); - const first_letter = await page.getByLabel("First Letter").first(); await country.fill("Canada"); await country.press("Enter"); await expect(city).toHaveValue("Toronto"); - await expect(first_letter).toHaveValue("T"); await country.fill("Pakistan"); await country.press("Enter"); await expect(city).toHaveValue("Karachi"); - await expect(first_letter).toHaveValue("K"); }); diff --git a/js/app/test/stream_audio_out.spec.ts b/js/app/test/stream_audio_out.spec.ts index 203905eb5c0a2..5d9832b1201e6 100644 --- a/js/app/test/stream_audio_out.spec.ts +++ b/js/app/test/stream_audio_out.spec.ts @@ -1,18 +1,41 @@ import { test, expect } from "@gradio/tootils"; -test("audio streams correctly", async ({ page }) => { - const uploader = await page.locator("input[type=file]"); - await uploader.setInputFiles(["../../test/test_files/audio_sample.wav"]); +test.skip("audio streams from wav file correctly", async ({ page }) => { + test.skip(!!process.env.CI, "Not supported in CI"); + await page.getByRole("gridcell", { name: "wav" }).first().click(); await page.getByRole("button", { name: "Stream as File" }).click(); - await page.waitForSelector("audio"); - const isAudioPlaying = await page.evaluate(async () => { - const audio = document.querySelector("audio"); - if (!audio) { - return false; - } - await audio.play(); - await new Promise((resolve) => setTimeout(resolve, 2000)); - return audio.currentTime > 0; - }); - await expect(isAudioPlaying).toBeTruthy(); + // @ts-ignore + await page + .locator("#stream_as_file_output audio") + .evaluate(async (el) => await el.play()); + await expect + .poll( + async () => + await page + .locator("#stream_as_file_output audio") + // @ts-ignore + .evaluate((el) => el.currentTime) + ) + .toBeGreaterThan(0); +}); + +test.skip("audio streams from wav file correctly as bytes", async ({ + page +}) => { + test.skip(!!process.env.CI, "Not supported in CI"); + await page.getByRole("gridcell", { name: "wav" }).first().click(); + await page.getByRole("button", { name: "Stream as Bytes" }).click(); + // @ts-ignore + await page + .locator("#stream_as_bytes_output audio") + .evaluate(async (el) => await el.play()); + await expect + .poll( + async () => + await page + .locator("#stream_as_bytes_output audio") + // @ts-ignore + .evaluate((el) => el.currentTime) + ) + .toBeGreaterThan(0); }); diff --git a/js/app/test/stream_video_out.spec.ts b/js/app/test/stream_video_out.spec.ts new file mode 100644 index 0000000000000..0fc5022cd95d9 --- /dev/null +++ b/js/app/test/stream_video_out.spec.ts @@ -0,0 +1,31 @@ +import { test, expect } from "@gradio/tootils"; + +test("video streams from ts files correctly", async ({ page }) => { + test.skip(!!process.env.CI, "Not supported in CI"); + await page.getByRole("gridcell", { name: "false" }).click(); + await page.getByRole("button", { name: "process video" }).click(); + await expect + .poll( + async () => + await page + .locator("#stream_video_output video") + // @ts-ignore + .evaluate((el) => el.currentTime) + ) + .toBeGreaterThan(0); +}); + +test("video streams from mp4 files correctly", async ({ page }) => { + test.skip(!!process.env.CI, "Not supported in CI"); + await page.getByRole("gridcell", { name: "true" }).click(); + await page.getByRole("button", { name: "process video" }).click(); + await expect + .poll( + async () => + await page + .locator("#stream_video_output video") + // @ts-ignore + .evaluate((el) => el.currentTime) + ) + .toBeGreaterThan(0); +}); diff --git a/js/audio/package.json b/js/audio/package.json index c046d507ba79b..e8e2b0ac97670 100644 --- a/js/audio/package.json +++ b/js/audio/package.json @@ -15,12 +15,13 @@ "@gradio/upload": "workspace:^", "@gradio/utils": "workspace:^", "@gradio/wasm": "workspace:^", + "@types/wavesurfer.js": "^6.0.10", "extendable-media-recorder": "^9.0.0", "extendable-media-recorder-wav-encoder": "^7.0.76", + "hls.js": "^1.5.13", "resize-observer-polyfill": "^1.5.1", "svelte-range-slider-pips": "^2.0.1", - "wavesurfer.js": "^7.4.2", - "@types/wavesurfer.js": "^6.0.10" + "wavesurfer.js": "^7.4.2" }, "devDependencies": { "@gradio/preview": "workspace:^" diff --git a/js/audio/player/AudioPlayer.svelte b/js/audio/player/AudioPlayer.svelte index acee42e71f226..c3c235350803f 100644 --- a/js/audio/player/AudioPlayer.svelte +++ b/js/audio/player/AudioPlayer.svelte @@ -11,6 +11,8 @@ import type { WaveformOptions } from "../shared/types"; import { createEventDispatcher } from "svelte"; + import Hls from "hls.js"; + export let value: null | FileData = null; $: url = value?.url; export let label: string; @@ -40,6 +42,9 @@ let show_volume_slider = false; + let audio_player: HTMLAudioElement; + let stream_active = false; + const dispatch = createEventDispatcher<{ stop: undefined; play: undefined; @@ -129,6 +134,7 @@ }; async function load_audio(data: string): Promise { + stream_active = false; await resolve_wasm_src(data).then((resolved_src) => { if (!resolved_src || value?.is_stream) return; return waveform?.load(resolved_src); @@ -137,6 +143,52 @@ $: url && load_audio(url); + function load_stream(value: FileData | null): void { + if (!value || !value.is_stream || !value.url) return; + if (!audio_player) return; + if (Hls.isSupported() && !stream_active) { + // Set config to start playback after 1 second of data received + const hls = new Hls({ + maxBufferLength: 1, + maxMaxBufferLength: 1, + lowLatencyMode: true + }); + hls.loadSource(value.url); + hls.attachMedia(audio_player); + hls.on(Hls.Events.MANIFEST_PARSED, function () { + if (waveform_settings.autoplay) audio_player.play(); + }); + hls.on(Hls.Events.ERROR, function (event, data) { + console.error("HLS error:", event, data); + if (data.fatal) { + switch (data.type) { + case Hls.ErrorTypes.NETWORK_ERROR: + console.error( + "Fatal network error encountered, trying to recover" + ); + hls.startLoad(); + break; + case Hls.ErrorTypes.MEDIA_ERROR: + console.error("Fatal media error encountered, trying to recover"); + hls.recoverMediaError(); + break; + default: + console.error("Fatal error, cannot recover"); + hls.destroy(); + break; + } + } + }); + stream_active = true; + } else if (!stream_active) { + audio_player.src = value.url; + if (waveform_settings.autoplay) audio_player.play(); + stream_active = true; + } + } + + $: load_stream(value); + onMount(() => { window.addEventListener("keydown", (e) => { if (!waveform || show_volume_slider) return; @@ -149,20 +201,19 @@ }); +

diff --git a/js/audio/static/StaticAudio.svelte b/js/audio/static/StaticAudio.svelte index dd2c5a20eaa2e..7729fb968f86b 100644 --- a/js/audio/static/StaticAudio.svelte +++ b/js/audio/static/StaticAudio.svelte @@ -42,7 +42,12 @@ {#if value !== null}
{#if show_download_button} - + {/if} diff --git a/js/multimodaltextbox/Example.svelte b/js/multimodaltextbox/Example.svelte index dde751badf741..695eb161a095c 100644 --- a/js/multimodaltextbox/Example.svelte +++ b/js/multimodaltextbox/Example.svelte @@ -42,7 +42,7 @@ {#if file.mime_type && file.mime_type.includes("image")} {:else if file.mime_type && file.mime_type.includes("video")} -
diff --git a/js/video/package.json b/js/video/package.json index 70ed15868071c..66edf32d0cb43 100644 --- a/js/video/package.json +++ b/js/video/package.json @@ -7,6 +7,8 @@ "license": "ISC", "private": false, "dependencies": { + "@ffmpeg/ffmpeg": "^0.12.7", + "@ffmpeg/util": "^0.12.1", "@gradio/atoms": "workspace:^", "@gradio/client": "workspace:^", "@gradio/icons": "workspace:^", @@ -15,8 +17,7 @@ "@gradio/upload": "workspace:^", "@gradio/utils": "workspace:^", "@gradio/wasm": "workspace:^", - "@ffmpeg/ffmpeg": "^0.12.7", - "@ffmpeg/util": "^0.12.1", + "hls.js": "^1.5.13", "mrmime": "^2.0.0" }, "devDependencies": { diff --git a/js/video/shared/InteractiveVideo.svelte b/js/video/shared/InteractiveVideo.svelte index 684b51d5a96b7..a24f29c803223 100644 --- a/js/video/shared/InteractiveVideo.svelte +++ b/js/video/shared/InteractiveVideo.svelte @@ -119,6 +119,7 @@ {autoplay} src={value.url} subtitle={subtitle?.url} + is_stream={false} on:play on:pause on:stop diff --git a/js/video/shared/Player.svelte b/js/video/shared/Player.svelte index 46443d51ea4ab..499d4300cbfa7 100644 --- a/js/video/shared/Player.svelte +++ b/js/video/shared/Player.svelte @@ -18,6 +18,7 @@ export let handle_change: (video: FileData) => void = () => {}; export let handle_reset_value: () => void = () => {}; export let upload: Client["upload"]; + export let is_stream: boolean | undefined; const dispatch = createEventDispatcher<{ play: undefined; @@ -98,6 +99,7 @@ preload="auto" {autoplay} {loop} + {is_stream} on:click={play_pause} on:play on:pause diff --git a/js/video/shared/Video.svelte b/js/video/shared/Video.svelte index b2000986b5627..c3ac9009c3851 100644 --- a/js/video/shared/Video.svelte +++ b/js/video/shared/Video.svelte @@ -5,6 +5,8 @@ import { resolve_wasm_src } from "@gradio/wasm/svelte"; + import Hls from "hls.js"; + export let src: HTMLVideoAttributes["src"] = undefined; export let muted: HTMLVideoAttributes["muted"] = undefined; @@ -19,10 +21,12 @@ export let node: HTMLVideoElement | undefined = undefined; export let loop: boolean; + export let is_stream; export let processingVideo = false; let resolved_src: typeof src; + let stream_active = false; // The `src` prop can be updated before the Promise from `resolve_wasm_src` is resolved. // In such a case, the resolved value for the old `src` has to be discarded, @@ -46,6 +50,53 @@ } const dispatch = createEventDispatcher(); + + function load_stream( + src: string | null | undefined, + is_stream: boolean, + node: HTMLVideoElement | undefined + ): void { + if (!src || !is_stream) return; + if (!node) return; + if (Hls.isSupported() && !stream_active) { + const hls = new Hls({ + maxBufferLength: 1, // 0.5 seconds (500 ms) + maxMaxBufferLength: 1, // Maximum max buffer length in seconds + lowLatencyMode: true // Enable low latency mode + }); + hls.loadSource(src); + hls.attachMedia(node); + hls.on(Hls.Events.MANIFEST_PARSED, function () { + (node as HTMLVideoElement).play(); + }); + hls.on(Hls.Events.ERROR, function (event, data) { + console.error("HLS error:", event, data); + if (data.fatal) { + switch (data.type) { + case Hls.ErrorTypes.NETWORK_ERROR: + console.error( + "Fatal network error encountered, trying to recover" + ); + hls.startLoad(); + break; + case Hls.ErrorTypes.MEDIA_ERROR: + console.error("Fatal media error encountered, trying to recover"); + hls.recoverMediaError(); + break; + default: + console.error("Fatal error, cannot recover"); + hls.destroy(); + break; + } + } + }); + stream_active = true; + } + } + + $: src, (stream_active = false); + + $: load_stream(src, is_stream, node); diff --git a/js/image/shared/Webcam.svelte b/js/image/shared/Webcam.svelte index 9c218751d5b31..2349267197ea8 100644 --- a/js/image/shared/Webcam.svelte +++ b/js/image/shared/Webcam.svelte @@ -2,6 +2,7 @@ import { createEventDispatcher, onMount } from "svelte"; import { Camera, Circle, Square, DropdownArrow } from "@gradio/icons"; import type { I18nFormatter } from "@gradio/utils"; + import { StreamingBar } from "@gradio/statustracker"; import { type FileData, type Client, prepare_files } from "@gradio/client"; import WebcamPermissions from "./WebcamPermissions.svelte"; import { fade } from "svelte/transition"; @@ -14,11 +15,23 @@ let video_source: HTMLVideoElement; let available_video_devices: MediaDeviceInfo[] = []; let selected_device: MediaDeviceInfo | null = null; + let interval_id = 0; + let stop_button: HTMLButtonElement; + let time_limit: number | null = null; + + export const close_stream: () => void = () => { + time_limit = null; + }; + + export const set_time_limit = (time: number): void => { + if (recording) time_limit = time; + }; let canvas: HTMLCanvasElement; export let streaming = false; export let pending = false; export let root = ""; + export let stream_every = 1; export let mode: "image" | "video" = "image"; export let mirror_webcam: boolean; @@ -32,6 +45,7 @@ error: string; start_recording: undefined; stop_recording: undefined; + close_stream: undefined; }>(); onMount(() => (canvas = document.createElement("canvas"))); @@ -108,11 +122,15 @@ context.drawImage(video_source, -video_source.videoWidth, 0); } + if (streaming && !recording) { + return; + } + canvas.toBlob( (blob) => { - dispatch(streaming ? "stream" : "capture", blob); + dispatch("capture", blob); }, - "image/png", + `image/${streaming ? "jpeg" : "png"}`, 0.8 ); } @@ -181,6 +199,7 @@ take_recording(); } if (!recording && stream) { + dispatch("close_stream"); stream.getTracks().forEach((track) => track.stop()); video_source.srcObject = null; webcam_accessed = false; @@ -188,11 +207,11 @@ } if (streaming && mode === "image") { - window.setInterval(() => { + interval_id = window.setInterval(() => { if (video_source && !pending) { take_picture(); } - }, 500); + }, stream_every * 1000); } let options_open = false; @@ -225,6 +244,7 @@
+
- {#if selected_image?.caption} + {#if selected_media?.caption} - {selected_image.caption} + {selected_media.caption} {/if}
- {#each resolved_value as image, i} + {#each resolved_value as media, i} {/each}
@@ -347,7 +388,7 @@ (value = null)} + on:clear={() => (value = [])} />
{/if} @@ -369,13 +410,26 @@ on:click={() => (selected_index = i)} aria-label={"Thumbnail " + (i + 1) + " of " + resolved_value.length} > - {entry.caption + {#if "image" in entry} + {entry.caption + {:else} + +
{entry.caption} @@ -440,12 +494,13 @@ } } - .image-button { + .media-button { height: calc(100% - 60px); width: 100%; display: flex; } - .image-button :global(img) { + .media-button :global(img), + .media-button :global(video) { width: var(--size-full); height: var(--size-full); object-fit: contain; @@ -455,6 +510,14 @@ width: var(--size-full); height: var(--size-full); } + .thumbnails :global(svg) { + position: absolute; + top: var(--size-2); + left: var(--size-2); + width: 50%; + height: 50%; + opacity: 50%; + } .preview :global(img.with-caption) { height: var(--size-full); } @@ -516,6 +579,23 @@ border-color: var(--color-accent); } + .thumbnail-item :global(svg) { + position: absolute; + top: 50%; + left: 50%; + width: 50%; + height: 50%; + opacity: 50%; + transform: translate(-50%, -50%); + } + + .thumbnail-item :global(video) { + width: var(--size-full); + height: var(--size-full); + overflow: hidden; + object-fit: cover; + } + .thumbnail-small { flex: none; transform: scale(0.9); @@ -523,7 +603,6 @@ width: var(--size-9); height: var(--size-9); } - .thumbnail-small.selected { --ring-color: var(--color-accent); transform: scale(1); diff --git a/js/gallery/types.ts b/js/gallery/types.ts new file mode 100644 index 0000000000000..0ab7e98650085 --- /dev/null +++ b/js/gallery/types.ts @@ -0,0 +1,11 @@ +import type { FileData } from "@gradio/client"; + +export interface GalleryImage { + image: FileData; + caption: string | null; +} + +export interface GalleryVideo { + video: FileData; + caption: string | null; +} diff --git a/js/spa/test/gallery_component_events.spec.ts b/js/spa/test/gallery_component_events.spec.ts index f7a7a43176313..87e445ded53ac 100644 --- a/js/spa/test/gallery_component_events.spec.ts +++ b/js/spa/test/gallery_component_events.spec.ts @@ -1,14 +1,14 @@ import { test, expect } from "@gradio/tootils"; -test("Gallery preview mode displays all images correctly.", async ({ +test("Gallery preview mode displays all images/videos correctly.", async ({ page }) => { await page.getByRole("button", { name: "Run" }).click(); await page.getByLabel("Thumbnail 2 of 3").click(); await expect( - await page.getByTestId("detailed-image").getAttribute("src") - ).toEqual("https://gradio-builds.s3.amazonaws.com/assets/lite-logo.png"); + await page.getByTestId("detailed-video").getAttribute("src") + ).toEqual("https://gradio-static-files.s3.amazonaws.com/world.mp4"); await expect( await page.getByTestId("thumbnail 1").getAttribute("src") @@ -21,20 +21,20 @@ test("Gallery select event returns the right value and the download button works await page.getByRole("button", { name: "Run" }).click(); await page.getByLabel("Thumbnail 2 of 3").click(); await expect(page.getByLabel("Select Data")).toHaveValue( - "https://gradio-builds.s3.amazonaws.com/assets/lite-logo.png" + "https://gradio-static-files.s3.amazonaws.com/world.mp4" ); const downloadPromise = page.waitForEvent("download"); await page.getByLabel("Download").click(); const download = await downloadPromise; - expect(download.suggestedFilename()).toBe("lite-logo.png"); + expect(download.suggestedFilename()).toBe("world.mp4"); }); test("Gallery click-to-upload, upload and change events work correctly", async ({ page }) => { await page - .getByRole("button", { name: "Drop Image(s) Here - or - Click to Upload" }) + .getByRole("button", { name: "Drop Media Here - or - Click to Upload" }) .first() .click(); const uploader = await page.locator("input[type=file]").first(); diff --git a/js/spa/test/upload_file_limit_test.spec.ts b/js/spa/test/upload_file_limit_test.spec.ts index 30e8d2f381dcb..6e2886cfd37cb 100644 --- a/js/spa/test/upload_file_limit_test.spec.ts +++ b/js/spa/test/upload_file_limit_test.spec.ts @@ -58,7 +58,7 @@ test("gr.Gallery() triggers the gr.Error modal when an uploaded file exceeds max page }) => { const locator = page.getByText( - "Gallery Drop Image(s) Here - or - Click to Upload" + "Gallery Drop Media Here - or - Click to Upload" ); const file_chooser = await get_file_selector(page, locator); await file_chooser.setFiles(["./test/files/cheetah1.jpg"]); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 11d005b34b7b9..d4af0b8050341 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1280,6 +1280,9 @@ importers: '@gradio/utils': specifier: workspace:^ version: link:../utils + '@gradio/video': + specifier: workspace:^ + version: link:../video dequal: specifier: ^2.0.2 version: 2.0.2 diff --git a/test/components/test_gallery.py b/test/components/test_gallery.py index 157f25afc1972..9b9ca67022461 100644 --- a/test/components/test_gallery.py +++ b/test/components/test_gallery.py @@ -4,6 +4,7 @@ import PIL import gradio as gr +from gradio.components.gallery import GalleryImage from gradio.data_classes import FileData @@ -16,7 +17,7 @@ def test_postprocess(self): "image": { "path": url, "orig_name": "00015-20230906102032-7778-Wonderwoman VintageMagStyle _lora_SDXL-VintageMagStyle-Lora_1_, Very detailed, clean, high quality, sharp image.jpg", - "mime_type": None, + "mime_type": "image/jpeg", "size": None, "url": url, "is_stream": False, @@ -45,7 +46,7 @@ def test_gallery(self): "image": { "path": str(Path("test") / "test_files" / "foo.png"), "orig_name": "foo.png", - "mime_type": None, + "mime_type": "image/png", "size": None, "url": None, "is_stream": False, @@ -57,7 +58,7 @@ def test_gallery(self): "image": { "path": str(Path("test") / "test_files" / "bar.png"), "orig_name": "bar.png", - "mime_type": None, + "mime_type": "image/png", "size": None, "url": None, "is_stream": False, @@ -69,7 +70,7 @@ def test_gallery(self): "image": { "path": str(Path("test") / "test_files" / "baz.png"), "orig_name": "baz.png", - "mime_type": None, + "mime_type": "image/png", "size": None, "url": None, "is_stream": False, @@ -81,7 +82,7 @@ def test_gallery(self): "image": { "path": str(Path("test") / "test_files" / "qux.png"), "orig_name": "qux.png", - "mime_type": None, + "mime_type": "image/png", "size": None, "url": None, "is_stream": False, @@ -125,4 +126,5 @@ def test_gallery_format(self): output = gallery.postprocess( [np.random.randint(0, 255, (100, 100, 3), dtype=np.uint8)] ) - assert output.root[0].image.path.endswith(".jpeg") + if type(output.root[0]) == GalleryImage: + assert output.root[0].image.path.endswith(".jpeg") From 3d7a9b81f6fef06187eca832471dc1692eb493a0 Mon Sep 17 00:00:00 2001 From: Freddy Boulton Date: Tue, 20 Aug 2024 12:38:14 -0400 Subject: [PATCH 027/195] Open audio/image input stream only when queue is ready (#9149) * fix * submit logic happens in Blocks * add changeset * trigger ci * trigger ci * Add code * Add code * Fix retrigger refactor * Add code --------- Co-authored-by: gradio-pr-bot --- .changeset/easy-files-serve.md | 10 ++++ client/js/src/helpers/api_info.ts | 4 +- client/js/src/test/api_info.test.ts | 1 + client/js/src/types.ts | 1 + client/js/src/utils/submit.ts | 3 +- js/audio/Index.svelte | 12 +++- js/audio/interactive/InteractiveAudio.svelte | 18 +++++- js/audio/streaming/StreamAudio.svelte | 35 ++++++++++- js/core/src/Blocks.svelte | 47 ++++++++++++--- js/core/src/init.ts | 24 ++++++-- js/core/src/lang/en.json | 3 +- js/core/src/lang/zh-CN.json | 3 +- js/icons/src/Spinner.svelte | 40 +++++++++++++ js/icons/src/index.ts | 1 + js/image/Index.svelte | 12 +++- js/image/shared/ImageUploader.svelte | 4 +- js/image/shared/Webcam.svelte | 62 +++++++++++++++----- 17 files changed, 239 insertions(+), 41 deletions(-) create mode 100644 .changeset/easy-files-serve.md create mode 100644 js/icons/src/Spinner.svelte diff --git a/.changeset/easy-files-serve.md b/.changeset/easy-files-serve.md new file mode 100644 index 0000000000000..f8a389d5c3606 --- /dev/null +++ b/.changeset/easy-files-serve.md @@ -0,0 +1,10 @@ +--- +"@gradio/audio": minor +"@gradio/client": minor +"@gradio/core": minor +"@gradio/icons": minor +"@gradio/image": minor +"gradio": minor +--- + +feat:Open audio/image input stream only when queue is ready diff --git a/client/js/src/helpers/api_info.ts b/client/js/src/helpers/api_info.ts index 6d9d5bb37b0a1..b500c539fc018 100644 --- a/client/js/src/helpers/api_info.ts +++ b/client/js/src/helpers/api_info.ts @@ -247,6 +247,7 @@ export function handle_message( | "unexpected_error"; data?: any; status?: Status; + original_msg?: string; } { const queue = true; switch (data.msg) { @@ -373,7 +374,8 @@ export function handle_message( position: 0, success: data.success, eta: data.eta - } + }, + original_msg: "process_starts" }; } diff --git a/client/js/src/test/api_info.test.ts b/client/js/src/test/api_info.test.ts index 40e9d1f57fd5f..a717069723168 100644 --- a/client/js/src/test/api_info.test.ts +++ b/client/js/src/test/api_info.test.ts @@ -238,6 +238,7 @@ describe("handle_message", () => { const result = handle_message(data, last_status); expect(result).toEqual({ type: "update", + original_msg: "process_starts", status: { queue: true, stage: "pending", diff --git a/client/js/src/types.ts b/client/js/src/types.ts index 01afb0d39b783..bb3d38703e00f 100644 --- a/client/js/src/types.ts +++ b/client/js/src/types.ts @@ -363,6 +363,7 @@ export interface StatusMessage extends Status { type: "status"; endpoint: string; fn_index: number; + original_msg?: string; } export interface PayloadMessage extends Payload { diff --git a/client/js/src/utils/submit.ts b/client/js/src/utils/submit.ts index a109784fa01fa..afcf5e588e41a 100644 --- a/client/js/src/utils/submit.ts +++ b/client/js/src/utils/submit.ts @@ -598,7 +598,7 @@ export function submit( event_id_final = event_id; let callback = async function (_data: object): Promise { try { - const { type, status, data } = handle_message( + const { type, status, data, original_msg } = handle_message( _data, last_status[fn_index] ); @@ -614,6 +614,7 @@ export function submit( endpoint: _endpoint, fn_index, time: new Date(), + original_msg: original_msg, ...status }); } else if (type === "complete") { diff --git a/js/audio/Index.svelte b/js/audio/Index.svelte index 6bced2a49843e..f417f6df90602 100644 --- a/js/audio/Index.svelte +++ b/js/audio/Index.svelte @@ -41,7 +41,15 @@ export let streaming: boolean; export let stream_every: number; - export let close_stream: () => void; + let stream_state = "closed"; + let _modify_stream: (state: "open" | "closed" | "waiting") => void; + export function modify_stream_state( + state: "open" | "closed" | "waiting" + ): void { + stream_state = state; + _modify_stream(state); + } + export const get_stream_state: () => void = () => stream_state; export let set_time_limit: (time: number) => void; export let gradio: Gradio<{ input: never; @@ -245,7 +253,7 @@ {waveform_options} {trim_region_settings} {stream_every} - bind:close_stream + bind:modify_stream={_modify_stream} bind:set_time_limit upload={gradio.client.upload} stream_handler={gradio.client.stream} diff --git a/js/audio/interactive/InteractiveAudio.svelte b/js/audio/interactive/InteractiveAudio.svelte index 17325c4798896..d5a3b3adf6c74 100644 --- a/js/audio/interactive/InteractiveAudio.svelte +++ b/js/audio/interactive/InteractiveAudio.svelte @@ -41,10 +41,21 @@ export let stream_every: number; let time_limit: number | null = null; + let stream_state: "open" | "waiting" | "closed" = "closed"; - export const close_stream: () => void = () => { - time_limit = null; + export const modify_stream: (state: "open" | "closed" | "waiting") => void = ( + state: "open" | "closed" | "waiting" + ) => { + if (state === "closed") { + time_limit = null; + stream_state = "closed"; + } else if (state === "waiting") { + stream_state = "waiting"; + } else { + stream_state = "open"; + } }; + export const set_time_limit = (time: number): void => { if (recording) time_limit = time; }; @@ -60,6 +71,7 @@ let pending_stream: Uint8Array[] = []; let submit_pending_stream_on_pending_end = false; let inited = false; + let stream_open = false; const NUM_HEADER_BYTES = 44; let audio_chunks: Blob[] = []; @@ -167,6 +179,7 @@ pending_stream.push(payload); } else { let blobParts = [header].concat(pending_stream, [payload]); + if (!recording || stream_state === "waiting") return; dispatch_blob(blobParts, "stream"); pending_stream = []; } @@ -240,6 +253,7 @@ {i18n} {waveform_settings} {waveform_options} + waiting={stream_state === "waiting"} /> {:else} import { onMount } from "svelte"; import type { I18nFormatter } from "@gradio/utils"; + import { Spinner } from "@gradio/icons"; import WaveSurfer from "wavesurfer.js"; import RecordPlugin from "wavesurfer.js/dist/plugins/record.js"; import type { WaveformOptions } from "../shared/types"; @@ -15,6 +16,7 @@ export let waveform_options: WaveformOptions = { show_recording_waveform: true }; + export let waiting = false; let micWaveform: WaveSurfer; let waveformRecord: RecordPlugin; @@ -48,7 +50,7 @@ /> {/if}
- {#if recording} + {#if recording && !waiting} + {:else if recording && waiting} + {:else}
@@ -38,145 +46,105 @@ demo.launch() ### Behavior The data format accepted by the Chatbot is dictated by the `type` parameter. -This parameter can take two values, `'tuples'` and `'messages'`. - - -If `type` is `'tuples'`, then the data sent to/from the chatbot will be a list of tuples. -The first element of each tuple is the user message and the second element is the bot's response. -Each element can be a string (markdown/html is supported), -a tuple (in which case the first element is a filepath that will be displayed in the chatbot), -or a gradio component (see the Examples section for more details). +This parameter can take two values, `'tuples'` and `'messages'`. +The `'tuples'` type is deprecated and will be removed in a future version of Gradio. +### Message format If the `type` is `'messages'`, then the data sent to/from the chatbot will be a list of dictionaries with `role` and `content` keys. This format is compliant with the format expected by most LLM APIs (HuggingChat, OpenAI, Claude). -The `role` key is either `'user'` or `'`assistant'` and the `content` key can be a string (markdown/html supported), -a `FileDataDict` (to represent a file that is displayed in the chatbot - documented below), or a gradio component. +The `role` key is either `'user'` or `'assistant'` and the `content` key can be one of the following: +1. A string (markdown/html is also supported). +2. A dictionary with `path` and `alt_text` keys. In this case, the file at `path` will be displayed in the chat history. Image, audio, and video files are fully embedded and visualized in the chat bubble. +The `path` key can point to a valid publicly available URL. The `alt_text` key is optional but it's good practice to provide [alt text](https://en.wikipedia.org/wiki/Alt_attribute). +3. An instance of another Gradio component. -For convenience, you can use the `ChatMessage` dataclass so that your text editor can give you autocomplete hints and typechecks. +
+We will show examples for all three cases below - ```python -from gradio import ChatMessage - def generate_response(history): + # A plain text response history.append( - ChatMessage(role="assistant", - content="How can I help you?") - ) + {"role": "assistant", content="I am happy to provide you that report and plot."} + ) + # Embed the quaterly sales report in the chat + history.append( + {"role": "assistant", content={"path": "quaterly_sales.txt", "alt_text": "Sales Report for Q2 2024"}} + ) + # Make a plot of sales data + history.append( + {"role": "assistant", content=gr.Plot(value=make_plot_from_file('quaterly_sales.txt'))} + ) return history ``` -Additionally, when `type` is `messages`, you can provide additional metadata regarding any tools used to generate the response. -This is useful for displaying the thought process of LLM agents. For example, +For convenience, you can use the `ChatMessage` dataclass so that your text editor can give you autocomplete hints and typechecks. ```python +from gradio import ChatMessage + def generate_response(history): history.append( ChatMessage(role="assistant", - content="The weather API says it is 20 degrees Celcius in New York.", - metadata={"title": "🛠️ Used tool Weather API"}) + content="How can I help you?") ) return history ``` -Would be displayed as following: - -Gradio chatbot tool display - - -All of the types expected by the messages format are documented below: - -```python -class MetadataDict(TypedDict): - title: Union[str, None] - -class FileDataDict(TypedDict): - path: str # server filepath - url: NotRequired[Optional[str]] # normalised server url - size: NotRequired[Optional[int]] # size in bytes - orig_name: NotRequired[Optional[str]] # original filename - mime_type: NotRequired[Optional[str]] - is_stream: NotRequired[bool] - meta: dict[Literal["_type"], Literal["gradio.FileData"]] - +### Tuples format -class MessageDict(TypedDict): - content: str | FileDataDict | Component - role: Literal["user", "assistant", "system"] - metadata: NotRequired[MetadataDict] - - -@dataclass -class Metadata: - title: Optional[str] = None - - -@dataclass -class ChatMessage: - role: Literal["user", "assistant", "system"] - content: str | FileData | Component | FileDataDict | tuple | list - metadata: MetadataDict | Metadata = field(default_factory=Metadata) -``` +If `type` is `'tuples'`, then the data sent to/from the chatbot will be a list of tuples. +The first element of each tuple is the user message and the second element is the bot's response. +Each element can be a string (markdown/html is supported), +a tuple (in which case the first element is a filepath that will be displayed in the chatbot), +or a gradio component (see the Examples section for more details). + +### Initialization + -## **As input component**: {@html style_formatted_text(obj.preprocess.return_doc.doc)} -##### Your function should accept one of these types: -If `type` is `tuples` - +{#if obj.string_shortcuts && obj.string_shortcuts.length > 0} + +### Shortcuts + +{/if} -```python -from gradio import Component +### Examples -def predict( - value: list[list[str | tuple[str, str] | Component | None]] | None -): - ... -``` +** Displaying Thoughts/Tool Usage ** -If `type` is `messages` - +When `type` is `messages`, you can provide additional metadata regarding any tools used to generate the response. +This is useful for displaying the thought process of LLM agents. For example, ```python -from gradio import MessageDict - -def predict(value: list[MessageDict] | None): - ... +def generate_response(history): + history.append( + ChatMessage(role="assistant", + content="The weather API says it is 20 degrees Celcius in New York.", + metadata={"title": "🛠️ Used tool Weather API"}) + ) + return history ``` -
- -## **As output component**: {@html style_formatted_text(obj.postprocess.parameter_doc[0].doc)} -##### Your function should return one of these types: - -If `type` is `tuples` - -```python -def predict(···) -> list[list[str | tuple[str] | tuple[str, str] | None] | tuple] | None - ... - return value -``` +Would be displayed as following: -If `type` is `messages` - +Gradio chatbot tool display -from gradio import ChatMessage, MessageDict +You can also specify metadata with a plain python dictionary, ```python -def predict(···) - > list[MessageDict] | list[ChatMessage]: - ... +def generate_response(history): + history.append( + dict(role="assistant", + content="The weather API says it is 20 degrees Celcius in New York.", + metadata={"title": "🛠️ Used tool Weather API"}) + ) + return history ``` - -### Initialization - - - -{#if obj.string_shortcuts && obj.string_shortcuts.length > 0} - -### Shortcuts - -{/if} - -### Examples - **Using Gradio Components Inside `gr.Chatbot`** The `Chatbot` component supports using many of the core Gradio components (such as `gr.Image`, `gr.Plot`, `gr.Audio`, and `gr.HTML`) inside of the chatbot. Simply include one of these components in your list of tuples. Here's an example: diff --git a/js/_website/src/routes/[[version]]/docs/gradio/[doc]/+page.svelte b/js/_website/src/routes/[[version]]/docs/gradio/[doc]/+page.svelte index f7fbeba6d1682..a4b37a3397662 100644 --- a/js/_website/src/routes/[[version]]/docs/gradio/[doc]/+page.svelte +++ b/js/_website/src/routes/[[version]]/docs/gradio/[doc]/+page.svelte @@ -257,7 +257,7 @@
{all_headers.page_title.title} {#if all_headers.headers && all_headers.headers.length > 0} -
+
+ {minimum_value} + {maximum}
- - From 3c73f00e3016b16917ebfe0bad390f2dff683457 Mon Sep 17 00:00:00 2001 From: Hannah Date: Fri, 30 Aug 2024 01:13:48 +0200 Subject: [PATCH 044/195] =?UTF-8?q?=F0=9F=94=A1=20Update=20default=20core?= =?UTF-8?q?=20Gradio=20font=20=20(#9204)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * change sans font from Source Sans Pro to Asap * change misc refs to Source Sans Pro * add changeset * revert old changes * add changeset * replace asap with IBM Plex Sans * add changeset * repalce asaps with ibm plex * tweak --------- Co-authored-by: gradio-pr-bot --- .changeset/pink-shirts-fry.md | 11 +++++++++++ gradio/themes/base.py | 2 +- gradio/themes/default.py | 2 +- guides/09_other-tutorials/theming-guide.md | 2 +- guides/cn/07_other-tutorials/theming-guide.md | 2 +- js/_cdn-test/index.html | 2 +- js/_website/tailwind.config.cjs | 2 +- js/component-test/src/theme.css | 4 ++-- js/lite/src/theme.css | 4 ++-- js/storybook/themeLight.js | 2 +- js/theme/src/pollen.config.cjs | 2 +- 11 files changed, 23 insertions(+), 12 deletions(-) create mode 100644 .changeset/pink-shirts-fry.md diff --git a/.changeset/pink-shirts-fry.md b/.changeset/pink-shirts-fry.md new file mode 100644 index 0000000000000..4bc8b7a8da607 --- /dev/null +++ b/.changeset/pink-shirts-fry.md @@ -0,0 +1,11 @@ +--- +"@gradio/lite": minor +"@gradio/theme": minor +"@self/cdn-test": minor +"@self/component-test": minor +"@self/storybook": minor +"gradio": minor +"website": minor +--- + +feat:🔡 Update default core Gradio font diff --git a/gradio/themes/base.py b/gradio/themes/base.py index f3877f5f0f545..284b117a47587 100644 --- a/gradio/themes/base.py +++ b/gradio/themes/base.py @@ -352,7 +352,7 @@ def __init__( spacing_size: sizes.Size | str = sizes.spacing_md, radius_size: sizes.Size | str = sizes.radius_md, font: fonts.Font | str | Iterable[fonts.Font | str] = ( - fonts.GoogleFont("Source Sans Pro"), + fonts.GoogleFont("IBM Plex Sans"), "ui-sans-serif", "system-ui", "sans-serif", diff --git a/gradio/themes/default.py b/gradio/themes/default.py index bcc2224e31c04..392d7736e6b7d 100644 --- a/gradio/themes/default.py +++ b/gradio/themes/default.py @@ -17,7 +17,7 @@ def __init__( radius_size: sizes.Size | str = sizes.radius_md, text_size: sizes.Size | str = sizes.text_md, font: fonts.Font | str | Iterable[fonts.Font | str] = ( - fonts.GoogleFont("Source Sans Pro"), + fonts.GoogleFont("IBM Plex Sans"), "ui-sans-serif", "system-ui", "sans-serif", diff --git a/guides/09_other-tutorials/theming-guide.md b/guides/09_other-tutorials/theming-guide.md index 48fd97c5c8908..767a725040064 100644 --- a/guides/09_other-tutorials/theming-guide.md +++ b/guides/09_other-tutorials/theming-guide.md @@ -156,7 +156,7 @@ You could also create your own custom `Size` objects and pass them in. The final 2 constructor arguments set the fonts of the theme. You can pass a list of fonts to each of these arguments to specify fallbacks. If you provide a string, it will be loaded as a system font. If you provide a `gradio.themes.GoogleFont`, the font will be loaded from Google Fonts. -- `font`: This sets the primary font of the theme. In the default theme, this is set to `gradio.themes.GoogleFont("Source Sans Pro")`. +- `font`: This sets the primary font of the theme. In the default theme, this is set to `gradio.themes.GoogleFont("IBM Plex Sans")`. - `font_mono`: This sets the monospace font of the theme. In the default theme, this is set to `gradio.themes.GoogleFont("IBM Plex Mono")`. You could modify these values such as the following: diff --git a/guides/cn/07_other-tutorials/theming-guide.md b/guides/cn/07_other-tutorials/theming-guide.md index 89515a61bb023..f411f4203b758 100644 --- a/guides/cn/07_other-tutorials/theming-guide.md +++ b/guides/cn/07_other-tutorials/theming-guide.md @@ -156,7 +156,7 @@ with gr.Blocks(theme=gr.themes.Default(spacing_size=gr.themes.sizes.spacing_sm, 最后的 2 个构造函数参数设置主题的字体。您可以将一系列字体传递给这些参数,以指定回退字体。如果提供了字符串,它将被加载为系统字体。如果提供了 `gradio.themes.GoogleFont`,则将从 Google Fonts 加载该字体。 -- `font`:此设置主题的主要字体。在默认主题中,此值设置为 `gradio.themes.GoogleFont("Source Sans Pro")`。 +- `font`:此设置主题的主要字体。在默认主题中,此值设置为 `gradio.themes.GoogleFont("IBM Plex Sans")`。 - `font_mono`:此设置主题的等宽字体。在默认主题中,此值设置为 `gradio.themes.GoogleFont("IBM Plex Mono")`。 您可以修改这些值,例如以下方式: diff --git a/js/_cdn-test/index.html b/js/_cdn-test/index.html index ea91273f74ca8..21dfe14af3898 100644 --- a/js/_cdn-test/index.html +++ b/js/_cdn-test/index.html @@ -5,7 +5,7 @@ html { /* background: #111; */ /* color: #eee; */ - font-family: Source Sans Pro; + font-family: IBM Plex Sans; } diff --git a/js/_website/tailwind.config.cjs b/js/_website/tailwind.config.cjs index 9272f8a112401..b975c6a590204 100644 --- a/js/_website/tailwind.config.cjs +++ b/js/_website/tailwind.config.cjs @@ -10,7 +10,7 @@ module.exports = { theme: { extend: { fontFamily: { - sans: ["Source Sans Pro", ...defaultTheme.fontFamily.sans], + sans: ["IBM Plex Sans", ...defaultTheme.fontFamily.sans], mono: ["IBM Plex Mono", ...defaultTheme.fontFamily.mono] }, colors: { diff --git a/js/component-test/src/theme.css b/js/component-test/src/theme.css index f931dc90bd0e8..f773c3d5b8dc6 100644 --- a/js/component-test/src/theme.css +++ b/js/component-test/src/theme.css @@ -54,7 +54,7 @@ --text-lg: 16px; --text-xl: 22px; --text-xxl: 26px; - --font: "Source Sans Pro", "ui-sans-serif", "system-ui", sans-serif; + --font: "IBM Plex Sans", "ui-sans-serif", "system-ui", sans-serif; --font-mono: "IBM Plex Mono", "ui-monospace", "Consolas", monospace; --body-background-fill: var(--background-fill-primary); --body-text-color: var(--neutral-800); @@ -432,7 +432,7 @@ --text-lg: 16px; --text-xl: 22px; --text-xxl: 26px; - --font: "Source Sans Pro", "ui-sans-serif", "system-ui", sans-serif; + --font: "IBM Plex Sans", "ui-sans-serif", "system-ui", sans-serif; --font-mono: "IBM Plex Mono", "ui-monospace", "Consolas", monospace; --body-text-size: var(--text-md); --body-text-weight: 400; diff --git a/js/lite/src/theme.css b/js/lite/src/theme.css index 19e647060cfcc..d0667ad26dee4 100644 --- a/js/lite/src/theme.css +++ b/js/lite/src/theme.css @@ -54,7 +54,7 @@ --text-lg: 16px; --text-xl: 22px; --text-xxl: 26px; - --font: 'Source Sans Pro', 'ui-sans-serif', 'system-ui', sans-serif; + --font: 'IBM Plex Sans', 'ui-sans-serif', 'system-ui', sans-serif; --font-mono: 'IBM Plex Mono', 'ui-monospace', 'Consolas', monospace; --body-background-fill: var(--background-fill-primary); --body-text-color: var(--neutral-800); @@ -351,7 +351,7 @@ --text-lg: 16px; --text-xl: 22px; --text-xxl: 26px; - --font: 'Source Sans Pro', 'ui-sans-serif', 'system-ui', sans-serif; + --font: 'IBM Plex Sans', 'ui-sans-serif', 'system-ui', sans-serif; --font-mono: 'IBM Plex Mono', 'ui-monospace', 'Consolas', monospace; --body-text-size: var(--text-md); --body-text-weight: 400; diff --git a/js/storybook/themeLight.js b/js/storybook/themeLight.js index 51d5170f32fe0..29925b72a4c84 100644 --- a/js/storybook/themeLight.js +++ b/js/storybook/themeLight.js @@ -8,7 +8,7 @@ export default create({ brandImage: Logo, brandTarget: "_blank", - fontBase: '"Source Sans Pro", sans-serif', + fontBase: '"IBM Plex Sans", sans-serif', fontCode: "monospace", // theme colours diff --git a/js/theme/src/pollen.config.cjs b/js/theme/src/pollen.config.cjs index b1e0f949109cb..8cf39001733f7 100644 --- a/js/theme/src/pollen.config.cjs +++ b/js/theme/src/pollen.config.cjs @@ -11,7 +11,7 @@ module.exports = defineConfig((pollen) => { }, font: { ...pollen.font, - sans: `Source Sans Pro, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"`, + sans: `IBM Plex Sans, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"`, mono: `IBM Plex Mono, ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace` }, radius: { From e9e737eeeb61d0bbf43277c75b6ffed8b34aa445 Mon Sep 17 00:00:00 2001 From: Hannah Date: Fri, 30 Aug 2024 22:17:19 +0200 Subject: [PATCH 045/195] Redesign `gr.Button()` (#9167) * *add new button styling *add origin theme class with old button styling * add changeset * remove new colour * add changeset * color and radius tweaks * remove neutral hue change * *update button demo *style tweaks * format * fix test * use white text on primary btn * adjust primary orange * tweak colour * disabled fixes * refactor * refactor * refactor * refactor * remove non-button changes * test * revert test * make cancel btn darker in light mode * change button stories to interactive * fix slider test * fix test * tweak * tweak secondary colour to work with gr.group() * add changeset * tweak * tweak button hover grey --------- Co-authored-by: gradio-pr-bot Co-authored-by: pngwn --- .changeset/cold-lies-mate.md | 7 ++ demo/button_component/run.ipynb | 2 +- demo/button_component/run.py | 48 ++++++++++++- gradio/blocks.py | 1 + gradio/themes/__init__.py | 2 + gradio/themes/base.py | 103 +++++++++++++------------- gradio/themes/builder_app.py | 1 + gradio/themes/default.py | 51 ++++++++----- gradio/themes/origin.py | 112 +++++++++++++++++++++++++++++ gradio/themes/utils/colors.py | 4 +- js/button/Button.stories.svelte | 15 ++-- js/button/shared/Button.svelte | 40 ++++++++--- js/group/Index.svelte | 2 +- js/slider/Slider.component.spec.ts | 79 +++++++++----------- 14 files changed, 338 insertions(+), 129 deletions(-) create mode 100644 .changeset/cold-lies-mate.md create mode 100644 gradio/themes/origin.py diff --git a/.changeset/cold-lies-mate.md b/.changeset/cold-lies-mate.md new file mode 100644 index 0000000000000..04757a87d7a48 --- /dev/null +++ b/.changeset/cold-lies-mate.md @@ -0,0 +1,7 @@ +--- +"@gradio/button": minor +"@gradio/group": minor +"gradio": minor +--- + +feat:Redesign `gr.Button()` diff --git a/demo/button_component/run.ipynb b/demo/button_component/run.ipynb index 438e15668df96..81f0218f5bc0b 100644 --- a/demo/button_component/run.ipynb +++ b/demo/button_component/run.ipynb @@ -1 +1 @@ -{"cells": [{"cell_type": "markdown", "id": "302934307671667531413257853548643485645", "metadata": {}, "source": ["# Gradio Demo: button_component"]}, {"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 gradio as gr\n", "\n", "with gr.Blocks() as demo:\n", " gr.Button()\n", "\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: button_component"]}, {"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 gradio as gr\n", "\n", "icon = \"https://cdn.icon-icons.com/icons2/2620/PNG/512/among_us_player_red_icon_156942.png\"\n", "with gr.Blocks() as demo:\n", " with gr.Row():\n", " gr.Button(variant=\"primary\")\n", " gr.Button(variant=\"secondary\")\n", " gr.Button(variant=\"stop\")\n", " with gr.Row():\n", " gr.Button(variant=\"primary\", size=\"sm\")\n", " gr.Button(variant=\"secondary\", size=\"sm\")\n", " gr.Button(variant=\"stop\", size=\"sm\")\n", " with gr.Row():\n", " gr.Button(variant=\"primary\", icon=icon)\n", " gr.Button(variant=\"secondary\", icon=icon)\n", " gr.Button(variant=\"stop\", icon=icon)\n", "\n", " with gr.Row():\n", " gr.Button(variant=\"primary\", size=\"sm\", icon=icon)\n", " gr.Button(variant=\"secondary\", size=\"sm\", icon=icon)\n", " gr.Button(variant=\"stop\", size=\"sm\", icon=icon)\n", "\n", " with gr.Row():\n", " gr.Button(variant=\"primary\", icon=icon, interactive=False)\n", " gr.Button(variant=\"secondary\", icon=icon, interactive=False)\n", " gr.Button(variant=\"stop\", icon=icon, interactive=False)\n", "\n", " with gr.Row():\n", " gr.Button(variant=\"primary\", size=\"sm\", icon=icon, interactive=False)\n", " gr.Button(variant=\"secondary\", size=\"sm\", icon=icon, interactive=False)\n", " gr.Button(variant=\"stop\", size=\"sm\", icon=icon, interactive=False)\n", "\n", " with gr.Row():\n", " gr.Button(variant=\"primary\", interactive=False)\n", " gr.Button(variant=\"secondary\", interactive=False)\n", " gr.Button(variant=\"stop\", interactive=False)\n", "\n", " with gr.Row():\n", " gr.Button(variant=\"primary\", size=\"sm\", interactive=False)\n", " gr.Button(variant=\"secondary\", size=\"sm\", interactive=False)\n", " gr.Button(variant=\"stop\", size=\"sm\", interactive=False)\n", "\n", " with gr.Group():\n", " gr.Button(variant=\"primary\")\n", " gr.Button(variant=\"primary\")\n", " gr.Button(variant=\"secondary\")\n", " gr.Button(variant=\"secondary\")\n", " gr.Button(variant=\"stop\")\n", " gr.Button(variant=\"stop\")\n", "\n", "\n", "demo.launch()\n"]}], "metadata": {}, "nbformat": 4, "nbformat_minor": 5} \ No newline at end of file diff --git a/demo/button_component/run.py b/demo/button_component/run.py index 9119c5c351d0c..88b0d2215517f 100644 --- a/demo/button_component/run.py +++ b/demo/button_component/run.py @@ -1,6 +1,52 @@ import gradio as gr +icon = "https://cdn.icon-icons.com/icons2/2620/PNG/512/among_us_player_red_icon_156942.png" with gr.Blocks() as demo: - gr.Button() + with gr.Row(): + gr.Button(variant="primary") + gr.Button(variant="secondary") + gr.Button(variant="stop") + with gr.Row(): + gr.Button(variant="primary", size="sm") + gr.Button(variant="secondary", size="sm") + gr.Button(variant="stop", size="sm") + with gr.Row(): + gr.Button(variant="primary", icon=icon) + gr.Button(variant="secondary", icon=icon) + gr.Button(variant="stop", icon=icon) + + with gr.Row(): + gr.Button(variant="primary", size="sm", icon=icon) + gr.Button(variant="secondary", size="sm", icon=icon) + gr.Button(variant="stop", size="sm", icon=icon) + + with gr.Row(): + gr.Button(variant="primary", icon=icon, interactive=False) + gr.Button(variant="secondary", icon=icon, interactive=False) + gr.Button(variant="stop", icon=icon, interactive=False) + + with gr.Row(): + gr.Button(variant="primary", size="sm", icon=icon, interactive=False) + gr.Button(variant="secondary", size="sm", icon=icon, interactive=False) + gr.Button(variant="stop", size="sm", icon=icon, interactive=False) + + with gr.Row(): + gr.Button(variant="primary", interactive=False) + gr.Button(variant="secondary", interactive=False) + gr.Button(variant="stop", interactive=False) + + with gr.Row(): + gr.Button(variant="primary", size="sm", interactive=False) + gr.Button(variant="secondary", size="sm", interactive=False) + gr.Button(variant="stop", size="sm", interactive=False) + + with gr.Group(): + gr.Button(variant="primary") + gr.Button(variant="primary") + gr.Button(variant="secondary") + gr.Button(variant="secondary") + gr.Button(variant="stop") + gr.Button(variant="stop") + demo.launch() diff --git a/gradio/blocks.py b/gradio/blocks.py index 864c27fbcd77a..bc103e3cd4930 100644 --- a/gradio/blocks.py +++ b/gradio/blocks.py @@ -111,6 +111,7 @@ themes.Monochrome(), themes.Soft(), themes.Glass(), + themes.Origin(), ] } diff --git a/gradio/themes/__init__.py b/gradio/themes/__init__.py index f7adbe74eef8e..673e03b20379c 100644 --- a/gradio/themes/__init__.py +++ b/gradio/themes/__init__.py @@ -2,6 +2,7 @@ from gradio.themes.default import Default from gradio.themes.glass import Glass from gradio.themes.monochrome import Monochrome +from gradio.themes.origin import Origin from gradio.themes.soft import Soft from gradio.themes.utils import colors, sizes from gradio.themes.utils.colors import Color @@ -21,6 +22,7 @@ "ThemeClass", "colors", "sizes", + "Origin", ] diff --git a/gradio/themes/base.py b/gradio/themes/base.py index 284b117a47587..a7f74acb7ef07 100644 --- a/gradio/themes/base.py +++ b/gradio/themes/base.py @@ -687,6 +687,8 @@ def set( button_border_width_dark=None, button_shadow=None, button_shadow_active=None, + button_transform=None, + button_active_transform=None, button_shadow_hover=None, button_transition=None, button_large_padding=None, @@ -972,6 +974,8 @@ def set( button_small_text_size: The text size of a button set to "small" size. button_small_text_weight: The text weight of a button set to "small" size. button_transition: The transition animation duration of a button between regular, hover, and focused states. + button_transform: The transform animation of a button on hover. + button_active_transform: The transform animation of a button when pressed. """ # Body @@ -1623,6 +1627,10 @@ def set( self.button_border_width_dark = button_border_width_dark or getattr( self, "button_border_width_dark", "*input_border_width" ) + self.button_shadow = button_shadow or getattr(self, "button_shadow", "none") + self.button_shadow_active = button_shadow_active or getattr( + self, "button_shadow_active", "none" + ) self.button_cancel_background_fill = button_cancel_background_fill or getattr( self, "button_cancel_background_fill", "*button_secondary_background_fill" ) @@ -1631,7 +1639,7 @@ def set( or getattr( self, "button_cancel_background_fill_dark", - "*button_secondary_background_fill", + "*neutral_700", ) ) self.button_cancel_background_fill_hover = ( @@ -1639,7 +1647,7 @@ def set( or getattr( self, "button_cancel_background_fill_hover", - "*button_cancel_background_fill", + "*button_secondary_background_fill_hover", ) ) self.button_cancel_background_fill_hover_dark = ( @@ -1647,7 +1655,7 @@ def set( or getattr( self, "button_cancel_background_fill_hover_dark", - "*button_cancel_background_fill", + "*button_secondary_background_fill_hover", ) ) self.button_cancel_border_color = button_cancel_border_color or getattr( @@ -1661,12 +1669,13 @@ def set( "*button_secondary_border_color", ) ) + self.button_cancel_border_color_hover = ( button_cancel_border_color_hover or getattr( self, "button_cancel_border_color_hover", - "*button_cancel_border_color", + "*button_secondary_border_color_hover", ) ) self.button_cancel_border_color_hover_dark = ( @@ -1674,9 +1683,10 @@ def set( or getattr( self, "button_cancel_border_color_hover_dark", - "*button_cancel_border_color", + "*button_secondary_border_color_hover", ) ) + self.button_cancel_text_color = button_cancel_text_color or getattr( self, "button_cancel_text_color", "*button_secondary_text_color" ) @@ -1684,19 +1694,31 @@ def set( self, "button_cancel_text_color_dark", "*button_secondary_text_color" ) self.button_cancel_text_color_hover = button_cancel_text_color_hover or getattr( - self, "button_cancel_text_color_hover", "*button_cancel_text_color" + self, "button_cancel_text_color_hover", "*button_secondary_text_color_hover" ) self.button_cancel_text_color_hover_dark = ( button_cancel_text_color_hover_dark - or getattr( - self, "button_cancel_text_color_hover_dark", "*button_cancel_text_color" - ) + or getattr(self, "button_cancel_text_color_hover_dark", "white") ) + + self.button_transform = button_transform or getattr( + self, "button_transform", "translateY(-0.5px)" + ) + self.button_active_transform = button_active_transform or getattr( + self, "button_active_transform", "translateY(-2px)" + ) + self.button_shadow_hover = button_shadow_hover or getattr( + self, "button_shadow_hover", "none" + ) + self.button_transition = button_transition or getattr( + self, "button_transition", "background-color 0.3s ease" + ) + self.button_large_padding = button_large_padding or getattr( self, "button_large_padding", "*spacing_lg calc(2 * *spacing_lg)" ) self.button_large_radius = button_large_radius or getattr( - self, "button_large_radius", "*radius_lg" + self, "button_large_radius", "*radius_md" ) self.button_large_text_size = button_large_text_size or getattr( self, "button_large_text_size", "*text_lg" @@ -1704,42 +1726,39 @@ def set( self.button_large_text_weight = button_large_text_weight or getattr( self, "button_large_text_weight", "600" ) + self.button_primary_background_fill = button_primary_background_fill or getattr( - self, "button_primary_background_fill", "*primary_200" + self, "button_primary_background_fill", "*primary_500" ) self.button_primary_background_fill_dark = ( button_primary_background_fill_dark - or getattr(self, "button_primary_background_fill_dark", "*primary_700") + or getattr(self, "button_primary_background_fill_dark", "*primary_600") ) self.button_primary_background_fill_hover = ( button_primary_background_fill_hover - or getattr( - self, - "button_primary_background_fill_hover", - "*button_primary_background_fill", - ) + or getattr(self, "button_primary_background_fill_hover", "*primary_600") ) self.button_primary_background_fill_hover_dark = ( button_primary_background_fill_hover_dark or getattr( self, "button_primary_background_fill_hover_dark", - "*button_primary_background_fill", + "*primary_700", ) ) self.button_primary_border_color = button_primary_border_color or getattr( - self, "button_primary_border_color", "*primary_200" + self, "button_primary_border_color", "transparent" ) self.button_primary_border_color_dark = ( button_primary_border_color_dark - or getattr(self, "button_primary_border_color_dark", "*primary_600") + or getattr(self, "button_primary_border_color_dark", "*primary_500") ) self.button_primary_border_color_hover = ( button_primary_border_color_hover or getattr( self, "button_primary_border_color_hover", - "*button_primary_border_color", + "*primary_700", ) ) self.button_primary_border_color_hover_dark = ( @@ -1751,7 +1770,7 @@ def set( ) ) self.button_primary_text_color = button_primary_text_color or getattr( - self, "button_primary_text_color", "*primary_600" + self, "button_primary_text_color", "white" ) self.button_primary_text_color_dark = button_primary_text_color_dark or getattr( self, "button_primary_text_color_dark", "white" @@ -1772,7 +1791,7 @@ def set( ) self.button_secondary_background_fill = ( button_secondary_background_fill - or getattr(self, "button_secondary_background_fill", "*neutral_200") + or getattr(self, "button_secondary_background_fill", "*neutral_300") ) self.button_secondary_background_fill_dark = ( button_secondary_background_fill_dark @@ -1780,33 +1799,27 @@ def set( ) self.button_secondary_background_fill_hover = ( button_secondary_background_fill_hover - or getattr( - self, - "button_secondary_background_fill_hover", - "*button_secondary_background_fill", - ) + or getattr(self, "button_secondary_background_fill_hover", "*neutral_400") ) self.button_secondary_background_fill_hover_dark = ( button_secondary_background_fill_hover_dark or getattr( - self, - "button_secondary_background_fill_hover_dark", - "*button_secondary_background_fill", + self, "button_secondary_background_fill_hover_dark", "*neutral_700" ) ) self.button_secondary_border_color = button_secondary_border_color or getattr( - self, "button_secondary_border_color", "*neutral_200" + self, "button_secondary_border_color", "*neutral_300" ) self.button_secondary_border_color_dark = ( button_secondary_border_color_dark - or getattr(self, "button_secondary_border_color_dark", "*neutral_600") + or getattr(self, "button_secondary_border_color_dark", "*neutral_500") ) self.button_secondary_border_color_hover = ( button_secondary_border_color_hover or getattr( self, "button_secondary_border_color_hover", - "*button_secondary_border_color", + "*neutral_500", ) ) self.button_secondary_border_color_hover_dark = ( @@ -1818,7 +1831,7 @@ def set( ) ) self.button_secondary_text_color = button_secondary_text_color or getattr( - self, "button_secondary_text_color", "*neutral_700" + self, "button_secondary_text_color", "black" ) self.button_secondary_text_color_dark = ( button_secondary_text_color_dark @@ -1840,26 +1853,18 @@ def set( "*button_secondary_text_color", ) ) - self.button_shadow = button_shadow or getattr(self, "button_shadow", "none") - self.button_shadow_active = button_shadow_active or getattr( - self, "button_shadow_active", "none" - ) - self.button_shadow_hover = button_shadow_hover or getattr( - self, "button_shadow_hover", "none" - ) + self.button_small_padding = button_small_padding or getattr( - self, "button_small_padding", "*spacing_sm calc(2 * *spacing_sm)" + self, "button_small_padding", "*spacing_sm calc(1.5 * *spacing_sm)" ) self.button_small_radius = button_small_radius or getattr( - self, "button_small_radius", "*radius_lg" + self, "button_small_radius", "*radius_md" ) self.button_small_text_size = button_small_text_size or getattr( - self, "button_small_text_size", "*text_md" + self, "button_small_text_size", "*text_sm" ) self.button_small_text_weight = button_small_text_weight or getattr( - self, "button_small_text_weight", "400" - ) - self.button_transition = button_transition or getattr( - self, "button_transition", "background-color 0.2s ease" + self, "button_small_text_weight", "600" ) + return self diff --git a/gradio/themes/builder_app.py b/gradio/themes/builder_app.py index 4c2c48371f0bc..c60a121dd28e4 100644 --- a/gradio/themes/builder_app.py +++ b/gradio/themes/builder_app.py @@ -12,6 +12,7 @@ gr.themes.Soft, gr.themes.Monochrome, gr.themes.Glass, + gr.themes.Origin, ] colors = gr.themes.Color.all sizes = gr.themes.Size.all diff --git a/gradio/themes/default.py b/gradio/themes/default.py index 392d7736e6b7d..9889ddfe53058 100644 --- a/gradio/themes/default.py +++ b/gradio/themes/default.py @@ -50,11 +50,11 @@ def __init__( error_icon_color=colors.red.c700, error_icon_color_dark=colors.red.c500, # Transition - button_transition="none", + button_transition="background-color 0.2s ease", # Shadows - button_shadow="*shadow_drop", - button_shadow_hover="*shadow_drop_lg", - button_shadow_active="*shadow_inset", + button_shadow="none", + button_shadow_hover="none", + button_shadow_active="none", input_shadow="0 0 0 *shadow_spread transparent, *shadow_inset", input_shadow_focus="0 0 0 *shadow_spread *secondary_50, *shadow_inset", input_shadow_focus_dark="0 0 0 *shadow_spread *neutral_700, *shadow_inset", @@ -71,22 +71,37 @@ def __init__( checkbox_label_background_fill_dark="linear-gradient(to top, *neutral_900, *neutral_800)", checkbox_label_background_fill_hover="linear-gradient(to top, *neutral_100, white)", checkbox_label_background_fill_hover_dark="linear-gradient(to top, *neutral_900, *neutral_800)", - button_primary_background_fill="linear-gradient(to bottom right, *primary_100, *primary_300)", - button_primary_background_fill_dark="linear-gradient(to bottom right, *primary_500, *primary_600)", - button_primary_background_fill_hover="linear-gradient(to bottom right, *primary_100, *primary_200)", - button_primary_background_fill_hover_dark="linear-gradient(to bottom right, *primary_500, *primary_500)", + # Primary Button + button_primary_background_fill="*primary_500", + button_primary_background_fill_dark="*primary_600", + button_primary_background_fill_hover="*primary_600", + button_primary_background_fill_hover_dark="*primary_700", + button_primary_border_color="*primary_500", button_primary_border_color_dark="*primary_500", - button_secondary_background_fill="linear-gradient(to bottom right, *neutral_100, *neutral_200)", - button_secondary_background_fill_dark="linear-gradient(to bottom right, *neutral_600, *neutral_700)", - button_secondary_background_fill_hover="linear-gradient(to bottom right, *neutral_100, *neutral_100)", - button_secondary_background_fill_hover_dark="linear-gradient(to bottom right, *neutral_600, *neutral_600)", - button_cancel_background_fill=f"linear-gradient(to bottom right, {colors.red.c100}, {colors.red.c200})", - button_cancel_background_fill_dark=f"linear-gradient(to bottom right, {colors.red.c600}, {colors.red.c700})", - button_cancel_background_fill_hover=f"linear-gradient(to bottom right, {colors.red.c100}, {colors.red.c100})", - button_cancel_background_fill_hover_dark=f"linear-gradient(to bottom right, {colors.red.c600}, {colors.red.c600})", - button_cancel_border_color=colors.red.c200, + button_primary_text_color="white", + button_primary_text_color_dark="white", + # Secondary Button + button_secondary_background_fill="*neutral_300", + button_secondary_background_fill_dark="*neutral_600", + button_secondary_background_fill_hover="*neutral_400", + button_secondary_background_fill_hover_dark="*neutral_700", + button_secondary_border_color="*neutral_300", + button_secondary_border_color_dark="*neutral_500", + button_secondary_text_color="black", + button_secondary_text_color_dark="white", + # Cancel Button + button_cancel_background_fill=colors.red.c500, + button_cancel_background_fill_dark=colors.red.c700, + button_cancel_background_fill_hover=colors.red.c600, + button_cancel_background_fill_hover_dark=colors.red.c800, + button_cancel_border_color=colors.red.c500, button_cancel_border_color_dark=colors.red.c600, - button_cancel_text_color=colors.red.c600, + button_cancel_border_color_hover=colors.red.c700, + button_cancel_border_color_hover_dark=colors.red.c600, + button_cancel_text_color="white", button_cancel_text_color_dark="white", + button_cancel_text_color_hover="white", + button_cancel_text_color_hover_dark="white", + # Other border_color_accent_subdued="*primary_200", ) diff --git a/gradio/themes/origin.py b/gradio/themes/origin.py new file mode 100644 index 0000000000000..ba96a7ab83f2e --- /dev/null +++ b/gradio/themes/origin.py @@ -0,0 +1,112 @@ +from __future__ import annotations + +from collections.abc import Iterable + +from gradio.themes.base import Base +from gradio.themes.utils import colors, fonts, sizes + + +class Origin(Base): + def __init__( + self, + *, + primary_hue: colors.Color | str = colors.orange, + secondary_hue: colors.Color | str = colors.blue, + neutral_hue: colors.Color | str = colors.gray, + spacing_size: sizes.Size | str = sizes.spacing_md, + radius_size: sizes.Size | str = sizes.radius_md, + text_size: sizes.Size | str = sizes.text_md, + font: fonts.Font | str | Iterable[fonts.Font | str] = ( + fonts.GoogleFont("Source Sans Pro"), + "ui-sans-serif", + "system-ui", + "sans-serif", + ), + font_mono: fonts.Font | str | Iterable[fonts.Font | str] = ( + fonts.GoogleFont("IBM Plex Mono"), + "ui-monospace", + "Consolas", + "monospace", + ), + ): + super().__init__( + primary_hue=primary_hue, + secondary_hue=secondary_hue, + neutral_hue=neutral_hue, + spacing_size=spacing_size, + radius_size=radius_size, + text_size=text_size, + font=font, + font_mono=font_mono, + ) + self.name = "origin" + super().set( + # Colors + input_background_fill_dark="*neutral_800", + error_background_fill=colors.red.c50, + error_background_fill_dark="*neutral_900", + error_border_color=colors.red.c700, + error_border_color_dark=colors.red.c500, + error_icon_color=colors.red.c700, + error_icon_color_dark=colors.red.c500, + # Transition + button_transition="none", + # Shadows + button_shadow="*shadow_drop", + button_shadow_hover="*shadow_drop_lg", + button_shadow_active="*shadow_inset", + input_shadow="0 0 0 *shadow_spread transparent, *shadow_inset", + input_shadow_focus="0 0 0 *shadow_spread *secondary_50, *shadow_inset", + input_shadow_focus_dark="0 0 0 *shadow_spread *neutral_700, *shadow_inset", + checkbox_label_shadow="*shadow_drop", + block_shadow="*shadow_drop", + form_gap_width="1px", + # Button borders + button_border_width="1px", + button_border_width_dark="1px", + input_border_width="1px", + input_background_fill="white", + # Gradients + stat_background_fill="linear-gradient(to right, *primary_400, *primary_200)", + stat_background_fill_dark="linear-gradient(to right, *primary_400, *primary_600)", + checkbox_label_background_fill="linear-gradient(to top, *neutral_50, white)", + checkbox_label_background_fill_dark="linear-gradient(to top, *neutral_900, *neutral_800)", + checkbox_label_background_fill_hover="linear-gradient(to top, *neutral_100, white)", + checkbox_label_background_fill_hover_dark="linear-gradient(to top, *neutral_900, *neutral_800)", + # Primary Button + button_primary_background_fill="linear-gradient(to bottom right, *primary_100, *primary_300)", + button_primary_background_fill_dark="linear-gradient(to bottom right, *primary_500, *primary_600)", + button_primary_background_fill_hover="linear-gradient(to bottom right, *primary_100, *primary_200)", + button_primary_background_fill_hover_dark="linear-gradient(to bottom right, *primary_500, *primary_500)", + button_primary_border_color="none", + button_primary_border_color_dark="*primary_500", + button_primary_border_color_hover="none", + button_primary_border_color_hover_dark="*primary_500", + button_primary_text_color="*primary_600", + button_primary_text_color_dark="white", + # Secondary Button + button_secondary_background_fill="linear-gradient(to bottom right, *neutral_100, *neutral_200)", + button_secondary_background_fill_dark="linear-gradient(to bottom right, *neutral_600, *neutral_700)", + button_secondary_background_fill_hover="linear-gradient(to bottom right, *neutral_100, *neutral_100)", + button_secondary_background_fill_hover_dark="linear-gradient(to bottom right, *neutral_600, *neutral_600)", + button_secondary_border_color="*neutral_200", + button_secondary_border_color_dark="*neutral_600", + button_secondary_border_color_hover="*neutral_200", + button_secondary_border_color_hover_dark="*neutral_600", + button_secondary_text_color="*neutral_800", + button_secondary_text_color_dark="white", + # Cancel Button + button_cancel_background_fill=f"linear-gradient(to bottom right, {colors.red.c100}, {colors.red.c200})", + button_cancel_background_fill_dark=f"linear-gradient(to bottom right, {colors.red.c600}, {colors.red.c700})", + button_cancel_background_fill_hover=f"linear-gradient(to bottom right, {colors.red.c100}, {colors.red.c100})", + button_cancel_background_fill_hover_dark=f"linear-gradient(to bottom right, {colors.red.c600}, {colors.red.c600})", + button_cancel_border_color=colors.red.c200, + button_cancel_border_color_dark=colors.red.c600, + button_cancel_border_color_hover=colors.red.c200, + button_cancel_border_color_hover_dark=colors.red.c600, + button_cancel_text_color=colors.red.c600, + button_cancel_text_color_dark="white", + # Other + border_color_accent_subdued="*primary_200", + button_transform="none", + ) diff --git a/gradio/themes/utils/colors.py b/gradio/themes/utils/colors.py index 6b2d975bdd524..51c67e242e8e7 100644 --- a/gradio/themes/utils/colors.py +++ b/gradio/themes/utils/colors.py @@ -83,7 +83,7 @@ def expand(self) -> list[str]: c100="#f4f4f5", c200="#e4e4e7", c300="#d4d4d8", - c400="#a1a1aa", + c400="#bbbbc2", c500="#71717a", c600="#52525b", c700="#3f3f46", @@ -137,7 +137,7 @@ def expand(self) -> list[str]: name="orange", c50="#fff7ed", c100="#ffedd5", - c200="#fed7aa", + c200="#ffddb3", c300="#fdba74", c400="#fb923c", c500="#f97316", diff --git a/js/button/Button.stories.svelte b/js/button/Button.stories.svelte index 8677141807a8f..85093cda0ff55 100644 --- a/js/button/Button.stories.svelte +++ b/js/button/Button.stories.svelte @@ -55,16 +55,23 @@
+ + diff --git a/js/chatbot/shared/ButtonPanel.svelte b/js/chatbot/shared/ButtonPanel.svelte index b1496e15132b9..a3c55a2d38835 100644 --- a/js/chatbot/shared/ButtonPanel.svelte +++ b/js/chatbot/shared/ButtonPanel.svelte @@ -6,13 +6,19 @@ import { DownloadLink } from "@gradio/wasm/svelte"; import type { NormalisedMessage, TextMessage } from "../types"; import { is_component_message } from "./utils"; + import ActionButton from "./ActionButton.svelte"; + import { Retry } from "@gradio/icons"; + import Remove from "./Remove.svelte"; export let likeable: boolean; + export let _retryable: boolean; + export let _undoable: boolean; export let show_copy_button: boolean; export let show: boolean; export let message: NormalisedMessage | NormalisedMessage[]; export let position: "right" | "left"; export let avatar: FileData | null; + export let disable: boolean; export let handle_action: (selected: string | null) => void; export let layout: "bubble" | "panel"; @@ -61,6 +67,26 @@ {/if} + {#if _retryable} + + + + {/if} + {#if _undoable} + + + + {/if} {#if likeable} {/if} @@ -81,10 +107,9 @@ border-radius: var(--radius-md); display: flex; align-items: center; - - height: var(--size-7); + height: var(--size-6); align-self: self-end; - margin: 0px calc(var(--spacing-xl) * 3); + margin: 0px calc(var(--spacing-xl) * 2); padding-left: 5px; z-index: 1; padding-bottom: var(--spacing-xl); diff --git a/js/chatbot/shared/ChatBot.svelte b/js/chatbot/shared/ChatBot.svelte index 52ee8b98f7728..60d1986589443 100644 --- a/js/chatbot/shared/ChatBot.svelte +++ b/js/chatbot/shared/ChatBot.svelte @@ -12,10 +12,9 @@ type ComponentType, tick } from "svelte"; - import { ShareButton } from "@gradio/atoms"; import { Image } from "@gradio/image/shared"; - import { Clear } from "@gradio/icons"; + import { Clear, Trash, Community } from "@gradio/icons"; import type { SelectData, LikeData } from "@gradio/utils"; import type { MessageRole } from "../types"; import { MarkdownCode as Markdown } from "@gradio/markdown"; @@ -23,6 +22,8 @@ import type { I18nFormatter } from "js/core/src/gradio_helper"; import Pending from "./Pending.svelte"; import MessageBox from "./MessageBox.svelte"; + import ActionButton from "./ActionButton.svelte"; + import { ShareError } from "@gradio/utils"; export let value: NormalisedMessage[] | null = []; let old_value: NormalisedMessage[] | null = null; @@ -79,6 +80,7 @@ display: boolean; }[]; export let pending_message = false; + export let generating = false; export let selectable = false; export let likeable = false; export let show_share_button = false; @@ -96,6 +98,8 @@ export let placeholder: string | null = null; export let upload: Client["upload"]; export let msg_format: "tuples" | "messages" = "tuples"; + export let _retryable = false; + export let _undoable = false; let target = document.querySelector("div.gradio-container"); @@ -134,6 +138,11 @@ change: undefined; select: SelectData; like: LikeData; + undo: undefined; + retry: undefined; + clear: undefined; + share: any; + error: string; }>(); beforeUpdate(() => { @@ -179,6 +188,21 @@ $: groupedMessages = value && group_messages(value); + function is_last_bot_message( + messages: NormalisedMessage[], + total_length: number + ): boolean { + const is_bot = messages[messages.length - 1].role === "assistant"; + const last_index = messages[messages.length - 1].index; + let is_last; + if (Array.isArray(last_index)) { + is_last = 2 * last_index[0] + last_index[1] === total_length - 1; + } else { + is_last = last_index === total_length - 1; + } + return is_last && is_bot; + } + function handle_select(i: number, message: NormalisedMessage): void { dispatch("select", { index: message.index, @@ -191,6 +215,14 @@ message: NormalisedMessage, selected: string | null ): void { + if (selected === "undo") { + dispatch("undo"); + return; + } else if (selected === "retry") { + dispatch("retry"); + return; + } + if (msg_format === "tuples") { dispatch("like", { index: message.index, @@ -266,22 +298,37 @@ } -{#if show_share_button && value !== null && value.length > 0} -