Spaces:
Running
Running
| import gradio as gr | |
| from app import demo as app | |
| import os | |
| _docs = { | |
| "Rerun": { | |
| "description": "Creates a Rerun viewer component that can be used to display the output of a Rerun stream.", | |
| "members": { | |
| "__init__": { | |
| "value": { | |
| "type": "list[pathlib.Path | str]\n | pathlib.Path\n | str\n | bytes\n | collections.abc.Callable\n | None", | |
| "default": "None", | |
| "description": "Takes a singular or list of RRD resources. Each RRD can be a Path, a string containing a url,", | |
| }, | |
| "label": { | |
| "type": "str | None", | |
| "default": "None", | |
| "description": "The label for this component. Appears above the component and is also used as the header if there", | |
| }, | |
| "every": { | |
| "type": "float | None", | |
| "default": "None", | |
| "description": "If `value` is a callable, run the function 'every' number of seconds while the client connection is", | |
| }, | |
| "show_label": { | |
| "type": "bool | None", | |
| "default": "None", | |
| "description": "if True, will display label.", | |
| }, | |
| "container": { | |
| "type": "bool", | |
| "default": "True", | |
| "description": "If True, will place the component in a container providing some extra padding around the border.", | |
| }, | |
| "scale": { | |
| "type": "int | None", | |
| "default": "None", | |
| "description": "relative size compared to adjacent Components.", | |
| }, | |
| "min_width": { | |
| "type": "int", | |
| "default": "160", | |
| "description": "minimum pixel width, will wrap if not sufficient screen space to satisfy this value.", | |
| }, | |
| "height": { | |
| "type": "int | str", | |
| "default": "640", | |
| "description": "height of component in pixels. If a string is provided, will be interpreted as a CSS value.", | |
| }, | |
| "visible": { | |
| "type": "bool", | |
| "default": "True", | |
| "description": "If False, component will be hidden.", | |
| }, | |
| "streaming": { | |
| "type": "bool", | |
| "default": "False", | |
| "description": "If True, the data should be incrementally yielded from the source as `bytes` returned by", | |
| }, | |
| "elem_id": { | |
| "type": "str | None", | |
| "default": "None", | |
| "description": "An optional string that is assigned as the id of this component in the HTML DOM.", | |
| }, | |
| "elem_classes": { | |
| "type": "list[str] | str | None", | |
| "default": "None", | |
| "description": "An optional list of strings that are assigned as the classes of this component in", | |
| }, | |
| "render": { | |
| "type": "bool", | |
| "default": "True", | |
| "description": "If False, component will not render be rendered in the Blocks context.", | |
| }, | |
| "panel_states": { | |
| "type": "dict[str, typing.Any] | None", | |
| "default": "None", | |
| "description": "Force viewer panels to a specific state.", | |
| }, | |
| }, | |
| "postprocess": { | |
| "value": { | |
| "type": "list[pathlib.Path | str] | pathlib.Path | str | bytes", | |
| "description": "The value to send over to the Rerun viewer on the front-end.", | |
| } | |
| }, | |
| "preprocess": { | |
| "return": { | |
| "type": "RerunData | None", | |
| "description": "A `RerunData` object.", | |
| }, | |
| "value": None, | |
| }, | |
| }, | |
| "events": { | |
| "play": { | |
| "type": None, | |
| "default": None, | |
| "description": "Fired when timeline playback starts. Callback should accept a parameter of type `gradio_rerun.events.Play`", | |
| }, | |
| "pause": { | |
| "type": None, | |
| "default": None, | |
| "description": "Fired when timeline pauseback starts. Callback should accept a parameter of type `gradio_rerun.events.Pause`", | |
| }, | |
| "time_update": { | |
| "type": None, | |
| "default": None, | |
| "description": "Fired when time updates. Callback should accept a parameter of type `gradio_rerun.events.TimeUpdate`.", | |
| }, | |
| "timeline_change": { | |
| "type": None, | |
| "default": None, | |
| "description": "Fired when a timeline is selected. Callback should accept a parameter of type `gradio_rerun.events.TimelineChange`.", | |
| }, | |
| "selection_change": { | |
| "type": None, | |
| "default": None, | |
| "description": "Fired when the selection changes. Callback should accept a parameter of type `gradio_rerun.events.SelectionChange`.", | |
| }, | |
| }, | |
| }, | |
| "__meta__": { | |
| "additional_interfaces": { | |
| "RerunData": { | |
| "source": "class RerunData(GradioRootModel):\n root: Sequence[FileData | Path | str] | None" | |
| } | |
| }, | |
| "user_fn_refs": {"Rerun": ["RerunData"]}, | |
| }, | |
| } | |
| abs_path = os.path.join(os.path.dirname(__file__), "css.css") | |
| with gr.Blocks( | |
| css=abs_path, | |
| theme=gr.themes.Default( | |
| font_mono=[ | |
| gr.themes.GoogleFont("Inconsolata"), | |
| "monospace", | |
| ], | |
| ), | |
| ) as demo: | |
| gr.Markdown( | |
| """ | |
| # `gradio_rerun` | |
| <div style="display: flex; gap: 7px;"> | |
| <a href="https://pypi.org/project/gradio_rerun/" target="_blank"><img alt="PyPI - Version" src="https://img.shields.io/pypi/v/gradio_rerun"></a> <a href="https://github.com/rerun-io/gradio-rerun-viewer/issues" target="_blank"><img alt="Static Badge" src="https://img.shields.io/badge/Issues-white?logo=github&logoColor=black"></a> <a href="https://huggingface.co/spaces/rerun/gradio-rerun-viewer/discussions" target="_blank"><img alt="Static Badge" src="https://img.shields.io/badge/%F0%9F%A4%97%20Discuss-%23097EFF?style=flat&logoColor=black"></a> | |
| </div> | |
| Rerun viewer with Gradio | |
| """, | |
| elem_classes=["md-custom"], | |
| header_links=True, | |
| ) | |
| app.render() | |
| gr.Markdown( | |
| """ | |
| ## Installation | |
| ```bash | |
| pip install gradio_rerun | |
| ``` | |
| ## Usage | |
| ```python | |
| \"\"\" | |
| Demonstrates integrating Rerun visualization with Gradio. | |
| Provides example implementations of data streaming, keypoint annotation, and dynamic | |
| visualization across multiple Gradio tabs using Rerun's recording and visualization capabilities. | |
| \"\"\" | |
| import math | |
| import os | |
| import tempfile | |
| import time | |
| import uuid | |
| import cv2 | |
| import gradio as gr | |
| import rerun as rr | |
| import rerun.blueprint as rrb | |
| from color_grid import build_color_grid | |
| from gradio_rerun import Rerun | |
| from gradio_rerun.events import ( | |
| SelectionChange, | |
| TimelineChange, | |
| TimeUpdate, | |
| ) | |
| # Whenever we need a recording, we construct a new recording stream. | |
| # As long as the app and recording IDs remain the same, the data | |
| # will be merged by the Viewer. | |
| def get_recording(recording_id: str) -> rr.RecordingStream: | |
| return rr.RecordingStream(application_id="rerun_example_gradio", recording_id=recording_id) | |
| # A task can directly log to a binary stream, which is routed to the embedded viewer. | |
| # Incremental chunks are yielded to the viewer using `yield stream.read()`. | |
| # | |
| # This is the preferred way to work with Rerun in Gradio since your data can be immediately and | |
| # incrementally seen by the viewer. Also, there are no ephemeral RRDs to cleanup or manage. | |
| def streaming_repeated_blur(recording_id: str, img): | |
| # Here we get a recording using the provided recording id. | |
| rec = get_recording(recording_id) | |
| stream = rec.binary_stream() | |
| if img is None: | |
| raise gr.Error("Must provide an image to blur.") | |
| blueprint = rrb.Blueprint( | |
| rrb.Horizontal( | |
| rrb.Spatial2DView(origin="image/original"), | |
| rrb.Spatial2DView(origin="image/blurred"), | |
| ), | |
| collapse_panels=True, | |
| ) | |
| rec.send_blueprint(blueprint) | |
| rec.set_time("iteration", sequence=0) | |
| rec.log("image/original", rr.Image(img)) | |
| yield stream.read() | |
| blur = img | |
| for i in range(100): | |
| rec.set_time("iteration", sequence=i) | |
| # Pretend blurring takes a while so we can see streaming in action. | |
| time.sleep(0.1) | |
| blur = cv2.GaussianBlur(blur, (5, 5), 0) | |
| rec.log("image/blurred", rr.Image(blur)) | |
| # Each time we yield bytes from the stream back to Gradio, they | |
| # are incrementally sent to the viewer. Make sure to yield any time | |
| # you want the user to be able to see progress. | |
| yield stream.read() | |
| # In this example the user is able to add keypoints to an image visualized in Rerun. | |
| # These keypoints are stored in the global state, we use the session id to keep track of which keypoints belong | |
| # to a specific session (https://www.gradio.app/guides/state-in-blocks). | |
| # | |
| # The current session can be obtained by adding a parameter of type `gradio.Request` to your event listener functions. | |
| Keypoint = tuple[float, float] | |
| keypoints_per_session_per_sequence_index: dict[str, dict[int, list[Keypoint]]] = {} | |
| def get_keypoints_for_user_at_sequence_index(request: gr.Request, sequence: int) -> list[Keypoint]: | |
| per_sequence = keypoints_per_session_per_sequence_index[request.session_hash] | |
| if sequence not in per_sequence: | |
| per_sequence[sequence] = [] | |
| return per_sequence[sequence] | |
| def initialize_instance(request: gr.Request) -> None: | |
| keypoints_per_session_per_sequence_index[request.session_hash] = {} | |
| def cleanup_instance(request: gr.Request) -> None: | |
| if request.session_hash in keypoints_per_session_per_sequence_index: | |
| del keypoints_per_session_per_sequence_index[request.session_hash] | |
| # In this function, the `request` and `evt` parameters will be automatically injected by Gradio when this | |
| # event listener is fired. | |
| # | |
| # `SelectionChange` is a subclass of `EventData`: https://www.gradio.app/docs/gradio/eventdata | |
| # `gr.Request`: https://www.gradio.app/main/docs/gradio/request | |
| def register_keypoint( | |
| active_recording_id: str, | |
| current_timeline: str, | |
| current_time: float, | |
| request: gr.Request, | |
| change: SelectionChange, | |
| ): | |
| if active_recording_id == "": | |
| return | |
| if current_timeline != "iteration": | |
| return | |
| evt = change.payload | |
| # We can only log a keypoint if the user selected only a single item. | |
| if len(evt.items) != 1: | |
| return | |
| item = evt.items[0] | |
| # If the selected item isn't an entity, or we don't have its position, then bail out. | |
| if item.type != "entity" or item.position is None: | |
| return | |
| # Now we can produce a valid keypoint. | |
| rec = get_recording(active_recording_id) | |
| stream = rec.binary_stream() | |
| # We round `current_time` toward 0, because that gives us the sequence index | |
| # that the user is currently looking at, due to the Viewer's latest-at semantics. | |
| index = math.floor(current_time) | |
| # We keep track of the keypoints per sequence index for each user manually. | |
| keypoints = get_keypoints_for_user_at_sequence_index(request, index) | |
| keypoints.append(item.position[0:2]) | |
| rec.set_time("iteration", sequence=index) | |
| rec.log(f"{item.entity_path}/keypoint", rr.Points2D(keypoints, radii=2)) | |
| yield stream.read() | |
| def track_current_time(evt: TimeUpdate): | |
| return evt.payload.time | |
| def track_current_timeline_and_time(evt: TimelineChange): | |
| return evt.payload.timeline, evt.payload.time | |
| # However, if you have a workflow that creates an RRD file instead, you can still send it | |
| # directly to the viewer by simply returning the path to the RRD file. | |
| # | |
| # This may be helpful if you need to execute a helper tool written in C++ or Rust that can't | |
| # be easily modified to stream data directly via Gradio. | |
| # | |
| # In this case you may want to clean up the RRD file after it's sent to the viewer so that you | |
| # don't accumulate too many temporary files. | |
| @rr.thread_local_stream("rerun_example_cube_rrd") | |
| def create_cube_rrd(x, y, z, pending_cleanup): | |
| cube = build_color_grid(int(x), int(y), int(z), twist=0) | |
| rr.log("cube", rr.Points3D(cube.positions, colors=cube.colors, radii=0.5)) | |
| # Simulate delay | |
| time.sleep(x / 10) | |
| # We eventually want to clean up the RRD file after it's sent to the viewer, so tracking | |
| # any pending files to be cleaned up when the state is deleted. | |
| temp = tempfile.NamedTemporaryFile(prefix="cube_", suffix=".rrd", delete=False) | |
| pending_cleanup.append(temp.name) | |
| blueprint = rrb.Spatial3DView(origin="cube") | |
| rr.save(temp.name, default_blueprint=blueprint) | |
| # Just return the name of the file -- Gradio will convert it to a FileData object | |
| # and send it to the viewer. | |
| return temp.name | |
| def cleanup_cube_rrds(pending_cleanup: list[str]) -> None: | |
| for f in pending_cleanup: | |
| os.unlink(f) | |
| with gr.Blocks() as demo: | |
| with gr.Tab("Streaming"): | |
| with gr.Row(): | |
| img = gr.Image(interactive=True, label="Image") | |
| with gr.Column(): | |
| stream_blur = gr.Button("Stream Repeated Blur") | |
| with gr.Row(): | |
| viewer = Rerun( | |
| streaming=True, | |
| panel_states={ | |
| "time": "collapsed", | |
| "blueprint": "hidden", | |
| "selection": "hidden", | |
| }, | |
| ) | |
| # We make a new recording id, and store it in a Gradio's session state. | |
| recording_id = gr.State(uuid.uuid4()) | |
| # Also store the current timeline and time of the viewer in the session state. | |
| current_timeline = gr.State("") | |
| current_time = gr.State(0.0) | |
| # When registering the event listeners, we pass the `recording_id` in as input in order to create | |
| # a recording stream using that id. | |
| stream_blur.click( | |
| # Using the `viewer` as an output allows us to stream data to it by yielding bytes from the callback. | |
| streaming_repeated_blur, | |
| inputs=[recording_id, img], | |
| outputs=[viewer], | |
| ) | |
| viewer.selection_change( | |
| register_keypoint, | |
| inputs=[recording_id, current_timeline, current_time], | |
| outputs=[viewer], | |
| ) | |
| viewer.time_update(track_current_time, outputs=[current_time]) | |
| viewer.timeline_change(track_current_timeline_and_time, outputs=[current_timeline, current_time]) | |
| with gr.Tab("Dynamic RRD"): | |
| pending_cleanup = gr.State([], time_to_live=10, delete_callback=cleanup_cube_rrds) | |
| with gr.Row(): | |
| x_count = gr.Number(minimum=1, maximum=10, value=5, precision=0, label="X Count") | |
| y_count = gr.Number(minimum=1, maximum=10, value=5, precision=0, label="Y Count") | |
| z_count = gr.Number(minimum=1, maximum=10, value=5, precision=0, label="Z Count") | |
| with gr.Row(): | |
| create_rrd = gr.Button("Create RRD") | |
| with gr.Row(): | |
| viewer = Rerun( | |
| streaming=True, | |
| panel_states={ | |
| "time": "collapsed", | |
| "blueprint": "hidden", | |
| "selection": "hidden", | |
| }, | |
| ) | |
| create_rrd.click( | |
| create_cube_rrd, | |
| inputs=[x_count, y_count, z_count, pending_cleanup], | |
| outputs=[viewer], | |
| ) | |
| with gr.Tab("Hosted RRD"): | |
| with gr.Row(): | |
| # It may be helpful to point the viewer to a hosted RRD file on another server. | |
| # If an RRD file is hosted via http, you can just return a URL to the file. | |
| choose_rrd = gr.Dropdown( | |
| label="RRD", | |
| choices=[ | |
| f"{rr.bindings.get_app_url()}/examples/arkit_scenes.rrd", | |
| f"{rr.bindings.get_app_url()}/examples/dna.rrd", | |
| f"{rr.bindings.get_app_url()}/examples/plots.rrd", | |
| ], | |
| ) | |
| with gr.Row(): | |
| viewer = Rerun( | |
| streaming=True, | |
| panel_states={ | |
| "time": "collapsed", | |
| "blueprint": "hidden", | |
| "selection": "hidden", | |
| }, | |
| ) | |
| choose_rrd.change(lambda x: x, inputs=[choose_rrd], outputs=[viewer]) | |
| demo.load(initialize_instance) | |
| demo.close(cleanup_instance) | |
| if __name__ == "__main__": | |
| demo.launch() | |
| ``` | |
| """, | |
| elem_classes=["md-custom"], | |
| header_links=True, | |
| ) | |
| gr.Markdown( | |
| """ | |
| ## `Rerun` | |
| ### Initialization | |
| """, | |
| elem_classes=["md-custom"], | |
| header_links=True, | |
| ) | |
| gr.ParamViewer(value=_docs["Rerun"]["members"]["__init__"], linkify=["RerunData"]) | |
| gr.Markdown("### Events") | |
| gr.ParamViewer(value=_docs["Rerun"]["events"], linkify=["Event"]) | |
| gr.Markdown( | |
| """ | |
| ### User function | |
| The impact on the users predict function varies depending on whether the component is used as an input or output for an event (or both). | |
| - When used as an Input, the component only impacts the input signature of the user function. | |
| - When used as an output, the component only impacts the return signature of the user function. | |
| The code snippet below is accurate in cases where the component is used as both an input and an output. | |
| - **As input:** Is passed, a `RerunData` object. | |
| - **As output:** Should return, the value to send over to the Rerun viewer on the front-end. | |
| ```python | |
| def predict( | |
| value: RerunData | None | |
| ) -> list[pathlib.Path | str] | pathlib.Path | str | bytes: | |
| return value | |
| ``` | |
| """, | |
| elem_classes=["md-custom", "Rerun-user-fn"], | |
| header_links=True, | |
| ) | |
| code_RerunData = gr.Markdown( | |
| """ | |
| ## `RerunData` | |
| ```python | |
| class RerunData(GradioRootModel): | |
| root: Sequence[FileData | Path | str] | None | |
| ```""", | |
| elem_classes=["md-custom", "RerunData"], | |
| header_links=True, | |
| ) | |
| demo.load( | |
| None, | |
| js=r"""function() { | |
| const refs = { | |
| RerunData: [], }; | |
| const user_fn_refs = { | |
| Rerun: ['RerunData'], }; | |
| requestAnimationFrame(() => { | |
| Object.entries(user_fn_refs).forEach(([key, refs]) => { | |
| if (refs.length > 0) { | |
| const el = document.querySelector(`.${key}-user-fn`); | |
| if (!el) return; | |
| refs.forEach(ref => { | |
| el.innerHTML = el.innerHTML.replace( | |
| new RegExp("\\b"+ref+"\\b", "g"), | |
| `<a href="#h-${ref.toLowerCase()}">${ref}</a>` | |
| ); | |
| }) | |
| } | |
| }) | |
| Object.entries(refs).forEach(([key, refs]) => { | |
| if (refs.length > 0) { | |
| const el = document.querySelector(`.${key}`); | |
| if (!el) return; | |
| refs.forEach(ref => { | |
| el.innerHTML = el.innerHTML.replace( | |
| new RegExp("\\b"+ref+"\\b", "g"), | |
| `<a href="#h-${ref.toLowerCase()}">${ref}</a>` | |
| ); | |
| }) | |
| } | |
| }) | |
| }) | |
| } | |
| """, | |
| ) | |
| demo.launch() | |