mirror of
https://github.com/gradio-app/gradio.git
synced 2024-12-15 02:11:15 +08:00
state.change listener with deep hash check (#8446)
* changes * changes * add changeset * changes * changes * changes * changes * Update guides/04_building-with-blocks/04_dynamic-apps-with-render-decorator.md Co-authored-by: Abubakar Abid <abubakar@huggingface.co> --------- Co-authored-by: Ali Abid <aliabid94@gmail.com> Co-authored-by: gradio-pr-bot <gradio-pr-bot@users.noreply.github.com> Co-authored-by: Abubakar Abid <abubakar@huggingface.co>
This commit is contained in:
parent
33c8081aa9
commit
4a55157ed9
5
.changeset/young-lamps-press.md
Normal file
5
.changeset/young-lamps-press.md
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
"gradio": minor
|
||||||
|
---
|
||||||
|
|
||||||
|
feat:state.change listener with deep hash check
|
@ -1 +1 @@
|
|||||||
{"cells": [{"cell_type": "markdown", "id": "302934307671667531413257853548643485645", "metadata": {}, "source": ["# Gradio Demo: audio_mixer"]}, {"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", "import numpy as np\n", "\n", "with gr.Blocks() as demo:\n", " track_count = gr.State(1)\n", " add_track_btn = gr.Button(\"Add Track\")\n", "\n", " add_track_btn.click(lambda count: count + 1, track_count, track_count)\n", "\n", " @gr.render(inputs=track_count)\n", " def render_tracks(count):\n", " audios = []\n", " volumes = []\n", " with gr.Row():\n", " for i in range(count):\n", " with gr.Column(variant=\"panel\", min_width=200):\n", " gr.Textbox(placeholder=\"Track Name\", key=f\"name-{i}\", show_label=False)\n", " track_audio = gr.Audio(label=f\"Track {i}\", key=f\"track-{i}\")\n", " track_volume = gr.Slider(0, 100, value=100, label=\"Volume\", key=f\"volume-{i}\")\n", " audios.append(track_audio)\n", " volumes.append(track_volume)\n", "\n", " def merge(data):\n", " sr, output = None, None\n", " for audio, volume in zip(audios, volumes):\n", " sr, audio_val = data[audio]\n", " volume_val = data[volume]\n", " final_track = audio_val * (volume_val / 100)\n", " if output is None:\n", " output = final_track\n", " else:\n", " min_shape = tuple(min(s1, s2) for s1, s2 in zip(output.shape, final_track.shape))\n", " trimmed_output = output[:min_shape[0], ...][:, :min_shape[1], ...] if output.ndim > 1 else output[:min_shape[0]]\n", " trimmed_final = final_track[:min_shape[0], ...][:, :final_track[1], ...] if final_track.ndim > 1 else final_track[:min_shape[0]]\n", " output += trimmed_output + trimmed_final\n", " return (sr, output)\n", " \n", " merge_btn.click(merge, set(audios + volumes), output_audio)\n", "\n", " merge_btn = gr.Button(\"Merge Tracks\")\n", " output_audio = gr.Audio(label=\"Output\")\n", " \n", "if __name__ == \"__main__\":\n", " demo.launch()"]}], "metadata": {}, "nbformat": 4, "nbformat_minor": 5}
|
{"cells": [{"cell_type": "markdown", "id": "302934307671667531413257853548643485645", "metadata": {}, "source": ["# Gradio Demo: audio_mixer"]}, {"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", "import numpy as np\n", "\n", "with gr.Blocks() as demo:\n", " track_count = gr.State(1)\n", " add_track_btn = gr.Button(\"Add Track\")\n", "\n", " add_track_btn.click(lambda count: count + 1, track_count, track_count)\n", "\n", " @gr.render(inputs=track_count)\n", " def render_tracks(count):\n", " audios = []\n", " volumes = []\n", " with gr.Row():\n", " for i in range(count):\n", " with gr.Column(variant=\"panel\", min_width=200):\n", " gr.Textbox(placeholder=\"Track Name\", key=f\"name-{i}\", show_label=False)\n", " track_audio = gr.Audio(label=f\"Track {i}\", key=f\"track-{i}\")\n", " track_volume = gr.Slider(0, 100, value=100, label=\"Volume\", key=f\"volume-{i}\")\n", " audios.append(track_audio)\n", " volumes.append(track_volume)\n", "\n", " def merge(data):\n", " sr, output = None, None\n", " for audio, volume in zip(audios, volumes):\n", " sr, audio_val = data[audio]\n", " volume_val = data[volume]\n", " final_track = audio_val * (volume_val / 100)\n", " if output is None:\n", " output = final_track\n", " else:\n", " min_shape = tuple(min(s1, s2) for s1, s2 in zip(output.shape, final_track.shape))\n", " trimmed_output = output[:min_shape[0], ...][:, :min_shape[1], ...] if output.ndim > 1 else output[:min_shape[0]]\n", " trimmed_final = final_track[:min_shape[0], ...][:, :final_track[1], ...] if final_track.ndim > 1 else final_track[:min_shape[0]]\n", " output += trimmed_output + trimmed_final\n", " return (sr, output)\n", " \n", " merge_btn.click(merge, set(audios + volumes), output_audio)\n", "\n", " merge_btn = gr.Button(\"Merge Tracks\")\n", " output_audio = gr.Audio(label=\"Output\", interactive=False)\n", " \n", "if __name__ == \"__main__\":\n", " demo.launch()"]}], "metadata": {}, "nbformat": 4, "nbformat_minor": 5}
|
@ -38,7 +38,7 @@ with gr.Blocks() as demo:
|
|||||||
merge_btn.click(merge, set(audios + volumes), output_audio)
|
merge_btn.click(merge, set(audios + volumes), output_audio)
|
||||||
|
|
||||||
merge_btn = gr.Button("Merge Tracks")
|
merge_btn = gr.Button("Merge Tracks")
|
||||||
output_audio = gr.Audio(label="Output")
|
output_audio = gr.Audio(label="Output", interactive=False)
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
demo.launch()
|
demo.launch()
|
@ -1 +1 @@
|
|||||||
{"cells": [{"cell_type": "markdown", "id": "302934307671667531413257853548643485645", "metadata": {}, "source": ["# Gradio Demo: state_change"]}, {"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", "\n", " with gr.Row():\n", " state_a = gr.State(0)\n", " btn_a = gr.Button(\"Increment A\")\n", " value_a = gr.Number(label=\"A\")\n", " btn_a.click(lambda x: x+1, state_a, state_a)\n", " state_a.change(lambda x: x, state_a, value_a)\n", " with gr.Row():\n", " state_b = gr.State(0)\n", " btn_b = gr.Button(\"Increment B\")\n", " value_b = gr.Number(label=\"num\")\n", " btn_b.click(lambda x: x+1, state_b, state_b)\n", "\n", " @gr.on(inputs=state_b, outputs=value_b)\n", " def identity(x):\n", " return x\n", "\n", " @gr.render(inputs=[state_a, state_b])\n", " def render(a, b):\n", " for x in range(a):\n", " with gr.Row():\n", " for y in range(b):\n", " gr.Button(f\"Button {x}, {y}\")\n", "\n", "if __name__ == \"__main__\":\n", " demo.launch()"]}], "metadata": {}, "nbformat": 4, "nbformat_minor": 5}
|
{"cells": [{"cell_type": "markdown", "id": "302934307671667531413257853548643485645", "metadata": {}, "source": ["# Gradio Demo: state_change"]}, {"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", "\n", " with gr.Row():\n", " state_a = gr.State(0)\n", " btn_a = gr.Button(\"Increment A\")\n", " value_a = gr.Number(label=\"A\")\n", " btn_a.click(lambda x: x+1, state_a, state_a)\n", " state_a.change(lambda x: x, state_a, value_a)\n", " with gr.Row():\n", " state_b = gr.State(0)\n", " btn_b = gr.Button(\"Increment B\")\n", " value_b = gr.Number(label=\"num\")\n", " btn_b.click(lambda x: x+1, state_b, state_b)\n", "\n", " @gr.on(inputs=state_b, outputs=value_b)\n", " def identity(x):\n", " return x\n", "\n", " @gr.render(inputs=[state_a, state_b])\n", " def render(a, b):\n", " for x in range(a):\n", " with gr.Row():\n", " for y in range(b):\n", " gr.Button(f\"Button {x}, {y}\")\n", "\n", " list_state = gr.State([])\n", " dict_state = gr.State(dict())\n", " nested_list_state = gr.State([])\n", " set_state = gr.State(set())\n", "\n", " def transform_list(x):\n", " return {n: n for n in x}, [x[:] for _ in range(len(x))], set(x)\n", " \n", " list_state.change(\n", " transform_list,\n", " inputs=list_state,\n", " outputs=[dict_state, nested_list_state, set_state],\n", " )\n", "\n", " all_textbox = gr.Textbox(label=\"Output\")\n", " change_count = gr.Number(label=\"Changes\")\n", " gr.on(\n", " inputs=[change_count, dict_state, nested_list_state, set_state],\n", " triggers=[dict_state.change, nested_list_state.change, set_state.change],\n", " fn=lambda x, *args: (x+1, \"\\n\".join(str(arg) for arg in args)),\n", " outputs=[change_count, all_textbox],\n", " )\n", "\n", " count_to_3_btn = gr.Button(\"Count to 3\")\n", " count_to_3_btn.click(lambda: [1, 2, 3], outputs=list_state)\n", " zero_all_btn = gr.Button(\"Zero All\")\n", " zero_all_btn.click(\n", " lambda x: [0] * len(x), inputs=list_state, outputs=list_state\n", " )\n", "\n", "if __name__ == \"__main__\":\n", " demo.launch()"]}], "metadata": {}, "nbformat": 4, "nbformat_minor": 5}
|
@ -25,5 +25,35 @@ with gr.Blocks() as demo:
|
|||||||
for y in range(b):
|
for y in range(b):
|
||||||
gr.Button(f"Button {x}, {y}")
|
gr.Button(f"Button {x}, {y}")
|
||||||
|
|
||||||
|
list_state = gr.State([])
|
||||||
|
dict_state = gr.State(dict())
|
||||||
|
nested_list_state = gr.State([])
|
||||||
|
set_state = gr.State(set())
|
||||||
|
|
||||||
|
def transform_list(x):
|
||||||
|
return {n: n for n in x}, [x[:] for _ in range(len(x))], set(x)
|
||||||
|
|
||||||
|
list_state.change(
|
||||||
|
transform_list,
|
||||||
|
inputs=list_state,
|
||||||
|
outputs=[dict_state, nested_list_state, set_state],
|
||||||
|
)
|
||||||
|
|
||||||
|
all_textbox = gr.Textbox(label="Output")
|
||||||
|
change_count = gr.Number(label="Changes")
|
||||||
|
gr.on(
|
||||||
|
inputs=[change_count, dict_state, nested_list_state, set_state],
|
||||||
|
triggers=[dict_state.change, nested_list_state.change, set_state.change],
|
||||||
|
fn=lambda x, *args: (x+1, "\n".join(str(arg) for arg in args)),
|
||||||
|
outputs=[change_count, all_textbox],
|
||||||
|
)
|
||||||
|
|
||||||
|
count_to_3_btn = gr.Button("Count to 3")
|
||||||
|
count_to_3_btn.click(lambda: [1, 2, 3], outputs=list_state)
|
||||||
|
zero_all_btn = gr.Button("Zero All")
|
||||||
|
zero_all_btn.click(
|
||||||
|
lambda x: [0] * len(x), inputs=list_state, outputs=list_state
|
||||||
|
)
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
demo.launch()
|
demo.launch()
|
@ -1 +1 @@
|
|||||||
{"cells": [{"cell_type": "markdown", "id": "302934307671667531413257853548643485645", "metadata": {}, "source": ["# Gradio Demo: todo_list"]}, {"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", " \n", " tasks = gr.State([])\n", " new_task = gr.Textbox(label=\"Task Name\", autofocus=True)\n", "\n", " def add_task(tasks, new_task_name):\n", " return tasks + [{\"name\": new_task_name, \"complete\": False}], \"\"\n", "\n", " new_task.submit(add_task, [tasks, new_task], [tasks, new_task])\n", "\n", " @gr.render(inputs=tasks)\n", " def render_todos(task_list):\n", " complete = [task for task in task_list if task[\"complete\"]]\n", " incomplete = [task for task in task_list if not task[\"complete\"]]\n", " gr.Markdown(f\"### Incomplete Tasks ({len(incomplete)})\")\n", " for task in incomplete:\n", " with gr.Row():\n", " gr.Textbox(task['name'], show_label=False, container=False)\n", " done_btn = gr.Button(\"Done\", scale=0)\n", " def mark_done(task=task):\n", " _task_list = task_list[:]\n", " _task_list[task_list.index(task)] = {\"name\": task[\"name\"], \"complete\": True}\n", " return _task_list\n", " done_btn.click(mark_done, None, [tasks])\n", "\n", " delete_btn = gr.Button(\"Delete\", scale=0, variant=\"stop\")\n", " def delete(task=task):\n", " task_index = task_list.index(task)\n", " return task_list[:task_index] + task_list[task_index+1:]\n", " delete_btn.click(delete, None, [tasks])\n", "\n", " gr.Markdown(f\"### Complete Tasks ({len(complete)})\")\n", " for task in complete:\n", " gr.Textbox(task['name'], show_label=False, container=False)\n", "\n", "\n", "\n", "\n", "if __name__ == \"__main__\":\n", " demo.launch()"]}], "metadata": {}, "nbformat": 4, "nbformat_minor": 5}
|
{"cells": [{"cell_type": "markdown", "id": "302934307671667531413257853548643485645", "metadata": {}, "source": ["# Gradio Demo: todo_list"]}, {"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", " \n", " tasks = gr.State([])\n", " new_task = gr.Textbox(label=\"Task Name\", autofocus=True)\n", "\n", " def add_task(tasks, new_task_name):\n", " return tasks + [{\"name\": new_task_name, \"complete\": False}], \"\"\n", "\n", " new_task.submit(add_task, [tasks, new_task], [tasks, new_task])\n", "\n", " @gr.render(inputs=tasks)\n", " def render_todos(task_list):\n", " complete = [task for task in task_list if task[\"complete\"]]\n", " incomplete = [task for task in task_list if not task[\"complete\"]]\n", " gr.Markdown(f\"### Incomplete Tasks ({len(incomplete)})\")\n", " for task in incomplete:\n", " with gr.Row():\n", " gr.Textbox(task['name'], show_label=False, container=False)\n", " done_btn = gr.Button(\"Done\", scale=0)\n", " def mark_done(task=task):\n", " task[\"complete\"] = True\n", " return task_list\n", " done_btn.click(mark_done, None, [tasks])\n", "\n", " delete_btn = gr.Button(\"Delete\", scale=0, variant=\"stop\")\n", " def delete(task=task):\n", " task_list.remove(task)\n", " return task_list\n", " delete_btn.click(delete, None, [tasks])\n", "\n", " gr.Markdown(f\"### Complete Tasks ({len(complete)})\")\n", " for task in complete:\n", " gr.Textbox(task['name'], show_label=False, container=False)\n", "\n", "\n", "\n", "\n", "if __name__ == \"__main__\":\n", " demo.launch()"]}], "metadata": {}, "nbformat": 4, "nbformat_minor": 5}
|
@ -20,15 +20,14 @@ with gr.Blocks() as demo:
|
|||||||
gr.Textbox(task['name'], show_label=False, container=False)
|
gr.Textbox(task['name'], show_label=False, container=False)
|
||||||
done_btn = gr.Button("Done", scale=0)
|
done_btn = gr.Button("Done", scale=0)
|
||||||
def mark_done(task=task):
|
def mark_done(task=task):
|
||||||
_task_list = task_list[:]
|
task["complete"] = True
|
||||||
_task_list[task_list.index(task)] = {"name": task["name"], "complete": True}
|
return task_list
|
||||||
return _task_list
|
|
||||||
done_btn.click(mark_done, None, [tasks])
|
done_btn.click(mark_done, None, [tasks])
|
||||||
|
|
||||||
delete_btn = gr.Button("Delete", scale=0, variant="stop")
|
delete_btn = gr.Button("Delete", scale=0, variant="stop")
|
||||||
def delete(task=task):
|
def delete(task=task):
|
||||||
task_index = task_list.index(task)
|
task_list.remove(task)
|
||||||
return task_list[:task_index] + task_list[task_index+1:]
|
return task_list
|
||||||
delete_btn.click(delete, None, [tasks])
|
delete_btn.click(delete, None, [tasks])
|
||||||
|
|
||||||
gr.Markdown(f"### Complete Tasks ({len(complete)})")
|
gr.Markdown(f"### Complete Tasks ({len(complete)})")
|
||||||
|
@ -1718,7 +1718,6 @@ Received outputs:
|
|||||||
self.validate_outputs(block_fn, predictions) # type: ignore
|
self.validate_outputs(block_fn, predictions) # type: ignore
|
||||||
|
|
||||||
output = []
|
output = []
|
||||||
changed_state_ids = []
|
|
||||||
for i, block in enumerate(block_fn.outputs):
|
for i, block in enumerate(block_fn.outputs):
|
||||||
try:
|
try:
|
||||||
if predictions[i] is components._Keywords.FINISHED_ITERATING:
|
if predictions[i] is components._Keywords.FINISHED_ITERATING:
|
||||||
@ -1732,16 +1731,6 @@ Received outputs:
|
|||||||
|
|
||||||
if block.stateful:
|
if block.stateful:
|
||||||
if not utils.is_update(predictions[i]):
|
if not utils.is_update(predictions[i]):
|
||||||
has_change_event = False
|
|
||||||
for dep in state.blocks_config.fns.values():
|
|
||||||
if block._id in [t[0] for t in dep.targets if t[1] == "change"]:
|
|
||||||
has_change_event = True
|
|
||||||
break
|
|
||||||
if has_change_event and (
|
|
||||||
block._id not in state
|
|
||||||
or not utils.deep_equal(state[block._id], predictions[i])
|
|
||||||
):
|
|
||||||
changed_state_ids.append(block._id)
|
|
||||||
state[block._id] = predictions[i]
|
state[block._id] = predictions[i]
|
||||||
output.append(None)
|
output.append(None)
|
||||||
else:
|
else:
|
||||||
@ -1785,7 +1774,7 @@ Received outputs:
|
|||||||
)
|
)
|
||||||
output.append(outputs_cached)
|
output.append(outputs_cached)
|
||||||
|
|
||||||
return output, changed_state_ids
|
return output
|
||||||
|
|
||||||
async def handle_streaming_outputs(
|
async def handle_streaming_outputs(
|
||||||
self,
|
self,
|
||||||
@ -1892,6 +1881,8 @@ Received outputs:
|
|||||||
if isinstance(block_fn, int):
|
if isinstance(block_fn, int):
|
||||||
block_fn = self.fns[block_fn]
|
block_fn = self.fns[block_fn]
|
||||||
batch = block_fn.batch
|
batch = block_fn.batch
|
||||||
|
state_ids_to_track, hashed_values = self.get_state_ids_to_track(block_fn, state)
|
||||||
|
changed_state_ids = []
|
||||||
|
|
||||||
if batch:
|
if batch:
|
||||||
max_batch_size = block_fn.max_batch_size
|
max_batch_size = block_fn.max_batch_size
|
||||||
@ -1924,11 +1915,10 @@ Received outputs:
|
|||||||
state,
|
state,
|
||||||
)
|
)
|
||||||
preds = result["prediction"]
|
preds = result["prediction"]
|
||||||
data_and_changed_state_ids = [
|
data = [
|
||||||
await self.postprocess_data(block_fn, list(o), state)
|
await self.postprocess_data(block_fn, list(o), state)
|
||||||
for o in zip(*preds)
|
for o in zip(*preds)
|
||||||
]
|
]
|
||||||
data, changed_state_ids = zip(*data_and_changed_state_ids)
|
|
||||||
if root_path is not None:
|
if root_path is not None:
|
||||||
data = processing_utils.add_root_url(data, root_path, None)
|
data = processing_utils.add_root_url(data, root_path, None)
|
||||||
data = list(zip(*data))
|
data = list(zip(*data))
|
||||||
@ -1952,9 +1942,14 @@ Received outputs:
|
|||||||
in_event_listener,
|
in_event_listener,
|
||||||
state,
|
state,
|
||||||
)
|
)
|
||||||
data, changed_state_ids = await self.postprocess_data(
|
data = await self.postprocess_data(block_fn, result["prediction"], state)
|
||||||
block_fn, result["prediction"], state
|
if state:
|
||||||
)
|
changed_state_ids = [
|
||||||
|
state_id
|
||||||
|
for hash_value, state_id in zip(hashed_values, state_ids_to_track)
|
||||||
|
if hash_value != utils.deep_hash(state[state_id])
|
||||||
|
]
|
||||||
|
|
||||||
if root_path is not None:
|
if root_path is not None:
|
||||||
data = processing_utils.add_root_url(data, root_path, None)
|
data = processing_utils.add_root_url(data, root_path, None)
|
||||||
is_generating, iterator = result["is_generating"], result["iterator"]
|
is_generating, iterator = result["is_generating"], result["iterator"]
|
||||||
@ -1995,6 +1990,22 @@ Received outputs:
|
|||||||
|
|
||||||
return output
|
return output
|
||||||
|
|
||||||
|
def get_state_ids_to_track(
|
||||||
|
self, block_fn: BlockFunction, state: SessionState | None
|
||||||
|
) -> tuple[list[int], list]:
|
||||||
|
if state is None:
|
||||||
|
return [], []
|
||||||
|
state_ids_to_track = []
|
||||||
|
hashed_values = []
|
||||||
|
for block in block_fn.outputs:
|
||||||
|
if block.stateful and any(
|
||||||
|
(block._id, "change") in fn.targets for fn in self.fns.values()
|
||||||
|
):
|
||||||
|
value = state[block._id]
|
||||||
|
state_ids_to_track.append(block._id)
|
||||||
|
hashed_values.append(utils.deep_hash(value))
|
||||||
|
return state_ids_to_track, hashed_values
|
||||||
|
|
||||||
def create_limiter(self):
|
def create_limiter(self):
|
||||||
self.limiter = (
|
self.limiter = (
|
||||||
None
|
None
|
||||||
|
@ -326,9 +326,7 @@ class Examples:
|
|||||||
[output.render() for output in self.outputs]
|
[output.render() for output in self.outputs]
|
||||||
demo.load(self.fn, self.inputs, self.outputs)
|
demo.load(self.fn, self.inputs, self.outputs)
|
||||||
demo.unrender()
|
demo.unrender()
|
||||||
return (await demo.postprocess_data(demo.default_config.fns[0], output, None))[
|
return await demo.postprocess_data(demo.default_config.fns[0], output, None)
|
||||||
0
|
|
||||||
]
|
|
||||||
|
|
||||||
def _get_cached_index_if_cached(self, example_index) -> int | None:
|
def _get_cached_index_if_cached(self, example_index) -> int | None:
|
||||||
if Path(self.cached_indices_file).exists():
|
if Path(self.cached_indices_file).exists():
|
||||||
|
@ -6,6 +6,7 @@ import ast
|
|||||||
import asyncio
|
import asyncio
|
||||||
import copy
|
import copy
|
||||||
import functools
|
import functools
|
||||||
|
import hashlib
|
||||||
import importlib
|
import importlib
|
||||||
import importlib.util
|
import importlib.util
|
||||||
import inspect
|
import inspect
|
||||||
@ -1402,3 +1403,25 @@ def connect_heartbeat(config: dict[str, Any], blocks) -> bool:
|
|||||||
if any_unload:
|
if any_unload:
|
||||||
break
|
break
|
||||||
return any_state or any_unload
|
return any_state or any_unload
|
||||||
|
|
||||||
|
|
||||||
|
def deep_hash(obj):
|
||||||
|
"""Compute a hash for a deeply nested data structure."""
|
||||||
|
hasher = hashlib.sha256()
|
||||||
|
if isinstance(obj, (int, float, str, bytes)):
|
||||||
|
items = obj
|
||||||
|
elif isinstance(obj, dict):
|
||||||
|
items = tuple(
|
||||||
|
[
|
||||||
|
(k, deep_hash(v))
|
||||||
|
for k, v in sorted(obj.items(), key=lambda x: hash(x[0]))
|
||||||
|
]
|
||||||
|
)
|
||||||
|
elif isinstance(obj, (list, tuple)):
|
||||||
|
items = tuple(deep_hash(x) for x in obj)
|
||||||
|
elif isinstance(obj, set):
|
||||||
|
items = tuple(deep_hash(x) for x in sorted(obj, key=hash))
|
||||||
|
else:
|
||||||
|
items = str(id(obj)).encode("utf-8")
|
||||||
|
hasher.update(repr(items).encode("utf-8"))
|
||||||
|
return hasher.hexdigest()
|
||||||
|
@ -51,7 +51,7 @@ $demo_todo_list
|
|||||||
|
|
||||||
Note that almost the entire app is inside a single `gr.render` that reacts to the tasks `gr.State` variable. This variable is a nested list, which presents some complexity. If you design a `gr.render` to react to a list or dict structure, ensure you do the following:
|
Note that almost the entire app is inside a single `gr.render` that reacts to the tasks `gr.State` variable. This variable is a nested list, which presents some complexity. If you design a `gr.render` to react to a list or dict structure, ensure you do the following:
|
||||||
|
|
||||||
1. Any event listener that modifies the state variable in a manner that should trigger a re-render should return a new value, not simply modify the state variable directly. A `gr.State` change event listener will not trigger if the state is provided as input and then modified directly. A new variable must be returned that can be compared against. That's why in the `mark_done` method, we create a new list that we edit. Similarly, in `delete`, we create a new list instead of calling `task_list.remove(task)`, and in `add_task`, we don't use `task_list.append`.
|
1. Any event listener that modifies a state variable in a manner that should trigger a re-render must set the state variable as an output. This lets Gradio know to check if the variable has changed behind the scenes.
|
||||||
2. In a `gr.render`, if a variable in a loop is used inside an event listener function, that variable should be "frozen" via setting it to itself as a default argument in the function header. See how we have `task=task` in both `mark_done` and `delete`. This freezes the variable to its "loop-time" value.
|
2. In a `gr.render`, if a variable in a loop is used inside an event listener function, that variable should be "frozen" via setting it to itself as a default argument in the function header. See how we have `task=task` in both `mark_done` and `delete`. This freezes the variable to its "loop-time" value.
|
||||||
|
|
||||||
Let's take a look at one last example that uses everything we learned. Below is an audio mixer. Provide multiple audio tracks and mix them together.
|
Let's take a look at one last example that uses everything we learned. Below is an audio mixer. Provide multiple audio tracks and mix them together.
|
||||||
|
39
js/app/test/state_change.spec.ts
Normal file
39
js/app/test/state_change.spec.ts
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
import { test, expect } from "@gradio/tootils";
|
||||||
|
|
||||||
|
test("test 2d state-based render", async ({ page }) => {
|
||||||
|
await page.getByRole("button", { name: "Increment A" }).click();
|
||||||
|
await expect(
|
||||||
|
page.locator("button").filter({ hasText: "Button" })
|
||||||
|
).toHaveCount(0);
|
||||||
|
await page.getByRole("button", { name: "Increment B" }).click();
|
||||||
|
await page.getByRole("button", { name: "Increment A" }).click();
|
||||||
|
await expect(
|
||||||
|
page.locator("button").filter({ hasText: "Button" })
|
||||||
|
).toHaveCount(2);
|
||||||
|
await page.getByRole("button", { name: "Increment A" }).click();
|
||||||
|
await page.getByRole("button", { name: "Increment B" }).click();
|
||||||
|
await page.getByRole("button", { name: "Increment A" }).click();
|
||||||
|
await expect(
|
||||||
|
page.locator("button").filter({ hasText: "Button" })
|
||||||
|
).toHaveCount(8);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("test datastructure-based state changes", async ({ page }) => {
|
||||||
|
await page.getByRole("button", { name: "Count to" }).click();
|
||||||
|
await expect(page.getByLabel("Output")).toHaveValue(
|
||||||
|
`{1: 1, 2: 2, 3: 3}\n[[1, 2, 3], [1, 2, 3], [1, 2, 3]]\n{1, 2, 3}`
|
||||||
|
);
|
||||||
|
await expect(page.getByLabel("Changes")).toHaveValue("1");
|
||||||
|
await page.getByRole("button", { name: "Count to" }).click();
|
||||||
|
await page.getByRole("button", { name: "Count to" }).click();
|
||||||
|
await page.getByRole("button", { name: "Count to" }).click();
|
||||||
|
await expect(page.getByLabel("Output")).toHaveValue(
|
||||||
|
`{1: 1, 2: 2, 3: 3}\n[[1, 2, 3], [1, 2, 3], [1, 2, 3]]\n{1, 2, 3}`
|
||||||
|
);
|
||||||
|
await expect(page.getByLabel("Changes")).toHaveValue("1");
|
||||||
|
await page.getByRole("button", { name: "Zero All" }).click();
|
||||||
|
await expect(page.getByLabel("Output")).toHaveValue(
|
||||||
|
`{0: 0}\n[[0, 0, 0], [0, 0, 0], [0, 0, 0]]\n{0}`
|
||||||
|
);
|
||||||
|
await expect(page.getByLabel("Changes")).toHaveValue("2");
|
||||||
|
});
|
50
js/app/test/todo_list.spec.ts
Normal file
50
js/app/test/todo_list.spec.ts
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
import { test, expect } from "@gradio/tootils";
|
||||||
|
|
||||||
|
test("clicking through tabs shows correct content", async ({ page }) => {
|
||||||
|
await expect(page.locator("body")).toContainText("Incomplete Tasks (0)");
|
||||||
|
await expect(page.locator("body")).toContainText("Complete Tasks (0)");
|
||||||
|
|
||||||
|
const input_text = page.getByLabel("Task Name");
|
||||||
|
|
||||||
|
await input_text.fill("eat");
|
||||||
|
await input_text.press("Enter");
|
||||||
|
|
||||||
|
await expect(page.locator("body")).not.toContainText("Incomplete Tasks (0)");
|
||||||
|
await expect(page.locator("body")).toContainText("Incomplete Tasks (1)");
|
||||||
|
await expect(page.locator("body")).toContainText("Complete Tasks (0)");
|
||||||
|
await expect(page.locator("textarea").nth(1)).toHaveValue("eat");
|
||||||
|
|
||||||
|
await input_text.fill("pray");
|
||||||
|
await input_text.press("Enter");
|
||||||
|
|
||||||
|
await expect(page.locator("body")).toContainText("Incomplete Tasks (2)");
|
||||||
|
await expect(page.locator("body")).toContainText("Complete Tasks (0)");
|
||||||
|
await expect(page.locator("textarea").nth(2)).toHaveValue("pray");
|
||||||
|
|
||||||
|
await input_text.fill("love");
|
||||||
|
await input_text.press("Enter");
|
||||||
|
|
||||||
|
await expect(page.locator("body")).toContainText("Incomplete Tasks (3)");
|
||||||
|
await expect(page.locator("body")).toContainText("Complete Tasks (0)");
|
||||||
|
await expect(page.locator("textarea").nth(1)).toHaveValue("eat");
|
||||||
|
await expect(page.locator("textarea").nth(2)).toHaveValue("pray");
|
||||||
|
await expect(page.locator("textarea").nth(3)).toHaveValue("love");
|
||||||
|
|
||||||
|
const done_btn_for_eat = page
|
||||||
|
.locator("button")
|
||||||
|
.filter({ hasText: "Done" })
|
||||||
|
.first();
|
||||||
|
await done_btn_for_eat.click();
|
||||||
|
|
||||||
|
await expect(page.locator("body")).toContainText("Incomplete Tasks (2)");
|
||||||
|
await expect(page.locator("body")).toContainText("Complete Tasks (1)");
|
||||||
|
|
||||||
|
const delete_btn_for_love = page
|
||||||
|
.locator("button")
|
||||||
|
.filter({ hasText: "Delete" })
|
||||||
|
.last();
|
||||||
|
await delete_btn_for_love.click();
|
||||||
|
|
||||||
|
await expect(page.locator("body")).toContainText("Incomplete Tasks (1)");
|
||||||
|
await expect(page.locator("body")).toContainText("Complete Tasks (1)");
|
||||||
|
});
|
@ -525,7 +525,7 @@ class TestBlocksPostprocessing:
|
|||||||
outputs=io_components,
|
outputs=io_components,
|
||||||
)
|
)
|
||||||
|
|
||||||
output, _ = await demo.postprocess_data(
|
output = await demo.postprocess_data(
|
||||||
demo.fns[0], [gr.update(value=None) for _ in io_components], state=None
|
demo.fns[0], [gr.update(value=None) for _ in io_components], state=None
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -550,7 +550,7 @@ class TestBlocksPostprocessing:
|
|||||||
outputs=text,
|
outputs=text,
|
||||||
)
|
)
|
||||||
|
|
||||||
output, _ = await demo.postprocess_data(
|
output = await demo.postprocess_data(
|
||||||
demo.fns[0], gr.update(value="NO_VALUE"), state=None
|
demo.fns[0], gr.update(value="NO_VALUE"), state=None
|
||||||
)
|
)
|
||||||
assert output[0]["value"] == "NO_VALUE"
|
assert output[0]["value"] == "NO_VALUE"
|
||||||
@ -566,7 +566,7 @@ class TestBlocksPostprocessing:
|
|||||||
checkbox = gr.Checkbox(value=True, label="Show image")
|
checkbox = gr.Checkbox(value=True, label="Show image")
|
||||||
checkbox.change(change_visibility, inputs=checkbox, outputs=im_list)
|
checkbox.change(change_visibility, inputs=checkbox, outputs=im_list)
|
||||||
|
|
||||||
output, _ = await demo.postprocess_data(
|
output = await demo.postprocess_data(
|
||||||
demo.fns[0], [gr.update(visible=False)] * 2, state=None
|
demo.fns[0], [gr.update(visible=False)] * 2, state=None
|
||||||
)
|
)
|
||||||
assert output == [
|
assert output == [
|
||||||
@ -586,12 +586,12 @@ class TestBlocksPostprocessing:
|
|||||||
|
|
||||||
update.click(update_values, inputs=[num], outputs=[num2])
|
update.click(update_values, inputs=[num], outputs=[num2])
|
||||||
|
|
||||||
output, _ = await demo.postprocess_data(
|
output = await demo.postprocess_data(
|
||||||
demo.fns[0], {num2: gr.Number(value=42)}, state=None
|
demo.fns[0], {num2: gr.Number(value=42)}, state=None
|
||||||
)
|
)
|
||||||
assert output[0]["value"] == 42
|
assert output[0]["value"] == 42
|
||||||
|
|
||||||
output, _ = await demo.postprocess_data(demo.fns[0], {num2: 23}, state=None)
|
output = await demo.postprocess_data(demo.fns[0], {num2: 23}, state=None)
|
||||||
assert output[0] == 23
|
assert output[0] == 23
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
@ -1695,7 +1695,7 @@ async def test_blocks_postprocessing_with_copies_of_component_instance():
|
|||||||
fn=clear_func, outputs=[chatbot, chatbot2, chatbot3], api_name="clear"
|
fn=clear_func, outputs=[chatbot, chatbot2, chatbot3], api_name="clear"
|
||||||
)
|
)
|
||||||
|
|
||||||
output, _ = await demo.postprocess_data(
|
output = await demo.postprocess_data(
|
||||||
demo.fns[0], [gr.Chatbot(value=[])] * 3, None
|
demo.fns[0], [gr.Chatbot(value=[])] * 3, None
|
||||||
)
|
)
|
||||||
assert output == [{"value": [], "__type__": "update"}] * 3
|
assert output == [{"value": [], "__type__": "update"}] * 3
|
||||||
|
Loading…
Reference in New Issue
Block a user