oxkitsune commited on
Commit
8a0e084
·
verified ·
1 Parent(s): d3ec363

Upload folder using huggingface_hub

Browse files
Files changed (8) hide show
  1. .gitignore +182 -0
  2. README.md +541 -6
  3. __init__.py +0 -0
  4. app.py +279 -0
  5. color_grid.py +47 -0
  6. css.css +157 -0
  7. requirements.txt +2 -0
  8. space.py +409 -0
.gitignore ADDED
@@ -0,0 +1,182 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Gradio
2
+ backend/**/templates
3
+ backend/**/*.pyi
4
+
5
+ # Frontend
6
+ node_modules
7
+
8
+
9
+ # Byte-compiled / optimized / DLL files
10
+ __pycache__/
11
+ *.py[cod]
12
+ *$py.class
13
+
14
+ # C extensions
15
+ *.so
16
+
17
+ # Distribution / packaging
18
+ .Python
19
+ build/
20
+ develop-eggs/
21
+ dist/
22
+ downloads/
23
+ eggs/
24
+ .eggs/
25
+ lib/
26
+ lib64/
27
+ parts/
28
+ sdist/
29
+ var/
30
+ wheels/
31
+ share/python-wheels/
32
+ *.egg-info/
33
+ .installed.cfg
34
+ *.egg
35
+ MANIFEST
36
+
37
+ # PyInstaller
38
+ # Usually these files are written by a python script from a template
39
+ # before PyInstaller builds the exe, so as to inject date/other infos into it.
40
+ *.manifest
41
+ *.spec
42
+
43
+ # Installer logs
44
+ pip-log.txt
45
+ pip-delete-this-directory.txt
46
+
47
+ # Unit test / coverage reports
48
+ htmlcov/
49
+ .tox/
50
+ .nox/
51
+ .coverage
52
+ .coverage.*
53
+ .cache
54
+ nosetests.xml
55
+ coverage.xml
56
+ *.cover
57
+ *.py,cover
58
+ .hypothesis/
59
+ .pytest_cache/
60
+ cover/
61
+
62
+ # Translations
63
+ *.mo
64
+ *.pot
65
+
66
+ # Django stuff:
67
+ *.log
68
+ local_settings.py
69
+ db.sqlite3
70
+ db.sqlite3-journal
71
+
72
+ # Flask stuff:
73
+ instance/
74
+ .webassets-cache
75
+
76
+ # Scrapy stuff:
77
+ .scrapy
78
+
79
+ # Sphinx documentation
80
+ docs/_build/
81
+
82
+ # PyBuilder
83
+ .pybuilder/
84
+ target/
85
+
86
+ # Jupyter Notebook
87
+ .ipynb_checkpoints
88
+
89
+ # IPython
90
+ profile_default/
91
+ ipython_config.py
92
+
93
+ # pyenv
94
+ # For a library or package, you might want to ignore these files since the code is
95
+ # intended to run in multiple environments; otherwise, check them in:
96
+ # .python-version
97
+
98
+ # pipenv
99
+ # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
100
+ # However, in case of collaboration, if having platform-specific dependencies or dependencies
101
+ # having no cross-platform support, pipenv may install dependencies that don't work, or not
102
+ # install all needed dependencies.
103
+ #Pipfile.lock
104
+
105
+ # UV
106
+ # Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control.
107
+ # This is especially recommended for binary packages to ensure reproducibility, and is more
108
+ # commonly ignored for libraries.
109
+ #uv.lock
110
+
111
+ # poetry
112
+ # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
113
+ # This is especially recommended for binary packages to ensure reproducibility, and is more
114
+ # commonly ignored for libraries.
115
+ # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
116
+ #poetry.lock
117
+
118
+ # pdm
119
+ # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
120
+ #pdm.lock
121
+ # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
122
+ # in version control.
123
+ # https://pdm.fming.dev/latest/usage/project/#working-with-version-control
124
+ .pdm.toml
125
+ .pdm-python
126
+ .pdm-build/
127
+
128
+ # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
129
+ __pypackages__/
130
+
131
+ # Celery stuff
132
+ celerybeat-schedule
133
+ celerybeat.pid
134
+
135
+ # SageMath parsed files
136
+ *.sage.py
137
+
138
+ # Environments
139
+ .env
140
+ .venv
141
+ env/
142
+ venv/
143
+ ENV/
144
+ env.bak/
145
+ venv.bak/
146
+
147
+ # Spyder project settings
148
+ .spyderproject
149
+ .spyproject
150
+
151
+ # Rope project settings
152
+ .ropeproject
153
+
154
+ # mkdocs documentation
155
+ /site
156
+
157
+ # mypy
158
+ .mypy_cache/
159
+ .dmypy.json
160
+ dmypy.json
161
+
162
+ # Pyre type checker
163
+ .pyre/
164
+
165
+ # pytype static type analyzer
166
+ .pytype/
167
+
168
+ # Cython debug symbols
169
+ cython_debug/
170
+
171
+ # PyCharm
172
+ # JetBrains specific template is maintained in a separate JetBrains.gitignore that can
173
+ # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
174
+ # and can be added to the global gitignore or merged into this file. For a more nuclear
175
+ # option (not recommended) you can uncomment the following to ignore the entire idea folder.
176
+ #.idea/
177
+
178
+ # Ruff stuff:
179
+ .ruff_cache/
180
+
181
+ # PyPI configuration file
182
+ .pypirc
README.md CHANGED
@@ -1,12 +1,547 @@
1
  ---
2
- title: Gradio Rerun
3
- emoji: 💻
4
- colorFrom: yellow
 
5
  colorTo: yellow
6
  sdk: gradio
7
- sdk_version: 5.26.0
8
- app_file: app.py
9
  pinned: false
 
10
  ---
11
 
12
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  ---
2
+ tags: [gradio-custom-component, SimpleImage, multimodal data, visualization, machine learning, robotics]
3
+ title: gradio_rerun
4
+ short_description: Rerun viewer with Gradio
5
+ colorFrom: blue
6
  colorTo: yellow
7
  sdk: gradio
 
 
8
  pinned: false
9
+ app_file: space.py
10
  ---
11
 
12
+ # `gradio_rerun`
13
+ <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>
14
+
15
+ Rerun viewer with Gradio
16
+
17
+ ## Installation
18
+
19
+ ```bash
20
+ pip install gradio_rerun
21
+ ```
22
+
23
+ ## Usage
24
+
25
+ ```python
26
+ """
27
+ Demonstrates integrating Rerun visualization with Gradio.
28
+
29
+ Provides example implementations of data streaming, keypoint annotation, and dynamic
30
+ visualization across multiple Gradio tabs using Rerun's recording and visualization capabilities.
31
+ """
32
+
33
+ import math
34
+ import os
35
+ import tempfile
36
+ import time
37
+ import uuid
38
+
39
+ import cv2
40
+ import gradio as gr
41
+ import rerun as rr
42
+ import rerun.blueprint as rrb
43
+ from color_grid import build_color_grid
44
+ from gradio_rerun import Rerun
45
+ from gradio_rerun.events import (
46
+ SelectionChange,
47
+ TimelineChange,
48
+ TimeUpdate,
49
+ )
50
+
51
+
52
+ # Whenever we need a recording, we construct a new recording stream.
53
+ # As long as the app and recording IDs remain the same, the data
54
+ # will be merged by the Viewer.
55
+ def get_recording(recording_id: str) -> rr.RecordingStream:
56
+ return rr.RecordingStream(application_id="rerun_example_gradio", recording_id=recording_id)
57
+
58
+
59
+ # A task can directly log to a binary stream, which is routed to the embedded viewer.
60
+ # Incremental chunks are yielded to the viewer using `yield stream.read()`.
61
+ #
62
+ # This is the preferred way to work with Rerun in Gradio since your data can be immediately and
63
+ # incrementally seen by the viewer. Also, there are no ephemeral RRDs to cleanup or manage.
64
+ def streaming_repeated_blur(recording_id: str, img):
65
+ # Here we get a recording using the provided recording id.
66
+ rec = get_recording(recording_id)
67
+ stream = rec.binary_stream()
68
+
69
+ if img is None:
70
+ raise gr.Error("Must provide an image to blur.")
71
+
72
+ blueprint = rrb.Blueprint(
73
+ rrb.Horizontal(
74
+ rrb.Spatial2DView(origin="image/original"),
75
+ rrb.Spatial2DView(origin="image/blurred"),
76
+ ),
77
+ collapse_panels=True,
78
+ )
79
+
80
+ rec.send_blueprint(blueprint)
81
+ rec.set_time("iteration", sequence=0)
82
+ rec.log("image/original", rr.Image(img))
83
+ yield stream.read()
84
+
85
+ blur = img
86
+ for i in range(100):
87
+ rec.set_time("iteration", sequence=i)
88
+
89
+ # Pretend blurring takes a while so we can see streaming in action.
90
+ time.sleep(0.1)
91
+ blur = cv2.GaussianBlur(blur, (5, 5), 0)
92
+ rec.log("image/blurred", rr.Image(blur))
93
+
94
+ # Each time we yield bytes from the stream back to Gradio, they
95
+ # are incrementally sent to the viewer. Make sure to yield any time
96
+ # you want the user to be able to see progress.
97
+ yield stream.read()
98
+
99
+
100
+ # In this example the user is able to add keypoints to an image visualized in Rerun.
101
+ # These keypoints are stored in the global state, we use the session id to keep track of which keypoints belong
102
+ # to a specific session (https://www.gradio.app/guides/state-in-blocks).
103
+ #
104
+ # The current session can be obtained by adding a parameter of type `gradio.Request` to your event listener functions.
105
+ Keypoint = tuple[float, float]
106
+ keypoints_per_session_per_sequence_index: dict[str, dict[int, list[Keypoint]]] = {}
107
+
108
+
109
+ def get_keypoints_for_user_at_sequence_index(request: gr.Request, sequence: int) -> list[Keypoint]:
110
+ per_sequence = keypoints_per_session_per_sequence_index[request.session_hash]
111
+ if sequence not in per_sequence:
112
+ per_sequence[sequence] = []
113
+
114
+ return per_sequence[sequence]
115
+
116
+
117
+ def initialize_instance(request: gr.Request) -> None:
118
+ keypoints_per_session_per_sequence_index[request.session_hash] = {}
119
+
120
+
121
+ def cleanup_instance(request: gr.Request) -> None:
122
+ if request.session_hash in keypoints_per_session_per_sequence_index:
123
+ del keypoints_per_session_per_sequence_index[request.session_hash]
124
+
125
+
126
+ # In this function, the `request` and `evt` parameters will be automatically injected by Gradio when this
127
+ # event listener is fired.
128
+ #
129
+ # `SelectionChange` is a subclass of `EventData`: https://www.gradio.app/docs/gradio/eventdata
130
+ # `gr.Request`: https://www.gradio.app/main/docs/gradio/request
131
+ def register_keypoint(
132
+ active_recording_id: str,
133
+ current_timeline: str,
134
+ current_time: float,
135
+ request: gr.Request,
136
+ change: SelectionChange,
137
+ ):
138
+ if active_recording_id == "":
139
+ return
140
+
141
+ if current_timeline != "iteration":
142
+ return
143
+
144
+ evt = change.payload
145
+
146
+ # We can only log a keypoint if the user selected only a single item.
147
+ if len(evt.items) != 1:
148
+ return
149
+ item = evt.items[0]
150
+
151
+ # If the selected item isn't an entity, or we don't have its position, then bail out.
152
+ if item.type != "entity" or item.position is None:
153
+ return
154
+
155
+ # Now we can produce a valid keypoint.
156
+ rec = get_recording(active_recording_id)
157
+ stream = rec.binary_stream()
158
+
159
+ # We round `current_time` toward 0, because that gives us the sequence index
160
+ # that the user is currently looking at, due to the Viewer's latest-at semantics.
161
+ index = math.floor(current_time)
162
+
163
+ # We keep track of the keypoints per sequence index for each user manually.
164
+ keypoints = get_keypoints_for_user_at_sequence_index(request, index)
165
+ keypoints.append(item.position[0:2])
166
+
167
+ rec.set_time("iteration", sequence=index)
168
+ rec.log(f"{item.entity_path}/keypoint", rr.Points2D(keypoints, radii=2))
169
+
170
+ yield stream.read()
171
+
172
+
173
+ def track_current_time(evt: TimeUpdate):
174
+ return evt.payload.time
175
+
176
+
177
+ def track_current_timeline_and_time(evt: TimelineChange):
178
+ return evt.payload.timeline, evt.payload.time
179
+
180
+
181
+ # However, if you have a workflow that creates an RRD file instead, you can still send it
182
+ # directly to the viewer by simply returning the path to the RRD file.
183
+ #
184
+ # This may be helpful if you need to execute a helper tool written in C++ or Rust that can't
185
+ # be easily modified to stream data directly via Gradio.
186
+ #
187
+ # In this case you may want to clean up the RRD file after it's sent to the viewer so that you
188
+ # don't accumulate too many temporary files.
189
+ @rr.thread_local_stream("rerun_example_cube_rrd")
190
+ def create_cube_rrd(x, y, z, pending_cleanup):
191
+ cube = build_color_grid(int(x), int(y), int(z), twist=0)
192
+ rr.log("cube", rr.Points3D(cube.positions, colors=cube.colors, radii=0.5))
193
+
194
+ # Simulate delay
195
+ time.sleep(x / 10)
196
+
197
+ # We eventually want to clean up the RRD file after it's sent to the viewer, so tracking
198
+ # any pending files to be cleaned up when the state is deleted.
199
+ temp = tempfile.NamedTemporaryFile(prefix="cube_", suffix=".rrd", delete=False)
200
+ pending_cleanup.append(temp.name)
201
+
202
+ blueprint = rrb.Spatial3DView(origin="cube")
203
+ rr.save(temp.name, default_blueprint=blueprint)
204
+
205
+ # Just return the name of the file -- Gradio will convert it to a FileData object
206
+ # and send it to the viewer.
207
+ return temp.name
208
+
209
+
210
+ def cleanup_cube_rrds(pending_cleanup: list[str]) -> None:
211
+ for f in pending_cleanup:
212
+ os.unlink(f)
213
+
214
+
215
+ with gr.Blocks() as demo:
216
+ with gr.Tab("Streaming"):
217
+ with gr.Row():
218
+ img = gr.Image(interactive=True, label="Image")
219
+ with gr.Column():
220
+ stream_blur = gr.Button("Stream Repeated Blur")
221
+
222
+ with gr.Row():
223
+ viewer = Rerun(
224
+ streaming=True,
225
+ panel_states={
226
+ "time": "collapsed",
227
+ "blueprint": "hidden",
228
+ "selection": "hidden",
229
+ },
230
+ )
231
+
232
+ # We make a new recording id, and store it in a Gradio's session state.
233
+ recording_id = gr.State(uuid.uuid4())
234
+
235
+ # Also store the current timeline and time of the viewer in the session state.
236
+ current_timeline = gr.State("")
237
+ current_time = gr.State(0.0)
238
+
239
+ # When registering the event listeners, we pass the `recording_id` in as input in order to create
240
+ # a recording stream using that id.
241
+ stream_blur.click(
242
+ # Using the `viewer` as an output allows us to stream data to it by yielding bytes from the callback.
243
+ streaming_repeated_blur,
244
+ inputs=[recording_id, img],
245
+ outputs=[viewer],
246
+ )
247
+ viewer.selection_change(
248
+ register_keypoint,
249
+ inputs=[recording_id, current_timeline, current_time],
250
+ outputs=[viewer],
251
+ )
252
+ viewer.time_update(track_current_time, outputs=[current_time])
253
+ viewer.timeline_change(track_current_timeline_and_time, outputs=[current_timeline, current_time])
254
+ with gr.Tab("Dynamic RRD"):
255
+ pending_cleanup = gr.State([], time_to_live=10, delete_callback=cleanup_cube_rrds)
256
+ with gr.Row():
257
+ x_count = gr.Number(minimum=1, maximum=10, value=5, precision=0, label="X Count")
258
+ y_count = gr.Number(minimum=1, maximum=10, value=5, precision=0, label="Y Count")
259
+ z_count = gr.Number(minimum=1, maximum=10, value=5, precision=0, label="Z Count")
260
+ with gr.Row():
261
+ create_rrd = gr.Button("Create RRD")
262
+ with gr.Row():
263
+ viewer = Rerun(
264
+ streaming=True,
265
+ panel_states={
266
+ "time": "collapsed",
267
+ "blueprint": "hidden",
268
+ "selection": "hidden",
269
+ },
270
+ )
271
+ create_rrd.click(
272
+ create_cube_rrd,
273
+ inputs=[x_count, y_count, z_count, pending_cleanup],
274
+ outputs=[viewer],
275
+ )
276
+
277
+ with gr.Tab("Hosted RRD"):
278
+ with gr.Row():
279
+ # It may be helpful to point the viewer to a hosted RRD file on another server.
280
+ # If an RRD file is hosted via http, you can just return a URL to the file.
281
+ choose_rrd = gr.Dropdown(
282
+ label="RRD",
283
+ choices=[
284
+ f"{rr.bindings.get_app_url()}/examples/arkit_scenes.rrd",
285
+ f"{rr.bindings.get_app_url()}/examples/dna.rrd",
286
+ f"{rr.bindings.get_app_url()}/examples/plots.rrd",
287
+ ],
288
+ )
289
+ with gr.Row():
290
+ viewer = Rerun(
291
+ streaming=True,
292
+ panel_states={
293
+ "time": "collapsed",
294
+ "blueprint": "hidden",
295
+ "selection": "hidden",
296
+ },
297
+ )
298
+ choose_rrd.change(lambda x: x, inputs=[choose_rrd], outputs=[viewer])
299
+ demo.load(initialize_instance)
300
+ demo.close(cleanup_instance)
301
+
302
+
303
+ if __name__ == "__main__":
304
+ demo.launch()
305
+
306
+ ```
307
+
308
+ ## `Rerun`
309
+
310
+ ### Initialization
311
+
312
+ <table>
313
+ <thead>
314
+ <tr>
315
+ <th align="left">name</th>
316
+ <th align="left" style="width: 25%;">type</th>
317
+ <th align="left">default</th>
318
+ <th align="left">description</th>
319
+ </tr>
320
+ </thead>
321
+ <tbody>
322
+ <tr>
323
+ <td align="left"><code>value</code></td>
324
+ <td align="left" style="width: 25%;">
325
+
326
+ ```python
327
+ list[pathlib.Path | str]
328
+ | pathlib.Path
329
+ | str
330
+ | bytes
331
+ | collections.abc.Callable
332
+ | None
333
+ ```
334
+
335
+ </td>
336
+ <td align="left"><code>None</code></td>
337
+ <td align="left">Takes a singular or list of RRD resources. Each RRD can be a Path, a string containing a url,</td>
338
+ </tr>
339
+
340
+ <tr>
341
+ <td align="left"><code>label</code></td>
342
+ <td align="left" style="width: 25%;">
343
+
344
+ ```python
345
+ str | None
346
+ ```
347
+
348
+ </td>
349
+ <td align="left"><code>None</code></td>
350
+ <td align="left">The label for this component. Appears above the component and is also used as the header if there</td>
351
+ </tr>
352
+
353
+ <tr>
354
+ <td align="left"><code>every</code></td>
355
+ <td align="left" style="width: 25%;">
356
+
357
+ ```python
358
+ float | None
359
+ ```
360
+
361
+ </td>
362
+ <td align="left"><code>None</code></td>
363
+ <td align="left">If `value` is a callable, run the function 'every' number of seconds while the client connection is</td>
364
+ </tr>
365
+
366
+ <tr>
367
+ <td align="left"><code>show_label</code></td>
368
+ <td align="left" style="width: 25%;">
369
+
370
+ ```python
371
+ bool | None
372
+ ```
373
+
374
+ </td>
375
+ <td align="left"><code>None</code></td>
376
+ <td align="left">if True, will display label.</td>
377
+ </tr>
378
+
379
+ <tr>
380
+ <td align="left"><code>container</code></td>
381
+ <td align="left" style="width: 25%;">
382
+
383
+ ```python
384
+ bool
385
+ ```
386
+
387
+ </td>
388
+ <td align="left"><code>True</code></td>
389
+ <td align="left">If True, will place the component in a container providing some extra padding around the border.</td>
390
+ </tr>
391
+
392
+ <tr>
393
+ <td align="left"><code>scale</code></td>
394
+ <td align="left" style="width: 25%;">
395
+
396
+ ```python
397
+ int | None
398
+ ```
399
+
400
+ </td>
401
+ <td align="left"><code>None</code></td>
402
+ <td align="left">relative size compared to adjacent Components.</td>
403
+ </tr>
404
+
405
+ <tr>
406
+ <td align="left"><code>min_width</code></td>
407
+ <td align="left" style="width: 25%;">
408
+
409
+ ```python
410
+ int
411
+ ```
412
+
413
+ </td>
414
+ <td align="left"><code>160</code></td>
415
+ <td align="left">minimum pixel width, will wrap if not sufficient screen space to satisfy this value.</td>
416
+ </tr>
417
+
418
+ <tr>
419
+ <td align="left"><code>height</code></td>
420
+ <td align="left" style="width: 25%;">
421
+
422
+ ```python
423
+ int | str
424
+ ```
425
+
426
+ </td>
427
+ <td align="left"><code>640</code></td>
428
+ <td align="left">height of component in pixels. If a string is provided, will be interpreted as a CSS value.</td>
429
+ </tr>
430
+
431
+ <tr>
432
+ <td align="left"><code>visible</code></td>
433
+ <td align="left" style="width: 25%;">
434
+
435
+ ```python
436
+ bool
437
+ ```
438
+
439
+ </td>
440
+ <td align="left"><code>True</code></td>
441
+ <td align="left">If False, component will be hidden.</td>
442
+ </tr>
443
+
444
+ <tr>
445
+ <td align="left"><code>streaming</code></td>
446
+ <td align="left" style="width: 25%;">
447
+
448
+ ```python
449
+ bool
450
+ ```
451
+
452
+ </td>
453
+ <td align="left"><code>False</code></td>
454
+ <td align="left">If True, the data should be incrementally yielded from the source as `bytes` returned by</td>
455
+ </tr>
456
+
457
+ <tr>
458
+ <td align="left"><code>elem_id</code></td>
459
+ <td align="left" style="width: 25%;">
460
+
461
+ ```python
462
+ str | None
463
+ ```
464
+
465
+ </td>
466
+ <td align="left"><code>None</code></td>
467
+ <td align="left">An optional string that is assigned as the id of this component in the HTML DOM.</td>
468
+ </tr>
469
+
470
+ <tr>
471
+ <td align="left"><code>elem_classes</code></td>
472
+ <td align="left" style="width: 25%;">
473
+
474
+ ```python
475
+ list[str] | str | None
476
+ ```
477
+
478
+ </td>
479
+ <td align="left"><code>None</code></td>
480
+ <td align="left">An optional list of strings that are assigned as the classes of this component in</td>
481
+ </tr>
482
+
483
+ <tr>
484
+ <td align="left"><code>render</code></td>
485
+ <td align="left" style="width: 25%;">
486
+
487
+ ```python
488
+ bool
489
+ ```
490
+
491
+ </td>
492
+ <td align="left"><code>True</code></td>
493
+ <td align="left">If False, component will not render be rendered in the Blocks context.</td>
494
+ </tr>
495
+
496
+ <tr>
497
+ <td align="left"><code>panel_states</code></td>
498
+ <td align="left" style="width: 25%;">
499
+
500
+ ```python
501
+ dict[str, typing.Any] | None
502
+ ```
503
+
504
+ </td>
505
+ <td align="left"><code>None</code></td>
506
+ <td align="left">Force viewer panels to a specific state.</td>
507
+ </tr>
508
+ </tbody></table>
509
+
510
+
511
+ ### Events
512
+
513
+ | name | description |
514
+ |:-----|:------------|
515
+ | `play` | Fired when timeline playback starts. Callback should accept a parameter of type `gradio_rerun.events.Play` |
516
+ | `pause` | Fired when timeline pauseback starts. Callback should accept a parameter of type `gradio_rerun.events.Pause` |
517
+ | `time_update` | Fired when time updates. Callback should accept a parameter of type `gradio_rerun.events.TimeUpdate`. |
518
+ | `timeline_change` | Fired when a timeline is selected. Callback should accept a parameter of type `gradio_rerun.events.TimelineChange`. |
519
+ | `selection_change` | Fired when the selection changes. Callback should accept a parameter of type `gradio_rerun.events.SelectionChange`. |
520
+
521
+
522
+
523
+ ### User function
524
+
525
+ 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).
526
+
527
+ - When used as an Input, the component only impacts the input signature of the user function.
528
+ - When used as an output, the component only impacts the return signature of the user function.
529
+
530
+ The code snippet below is accurate in cases where the component is used as both an input and an output.
531
+
532
+ - **As output:** Is passed, a `RerunData` object.
533
+ - **As input:** Should return, the value to send over to the Rerun viewer on the front-end.
534
+
535
+ ```python
536
+ def predict(
537
+ value: RerunData | None
538
+ ) -> list[pathlib.Path | str] | pathlib.Path | str | bytes:
539
+ return value
540
+ ```
541
+
542
+
543
+ ## `RerunData`
544
+ ```python
545
+ class RerunData(GradioRootModel):
546
+ root: Sequence[FileData | Path | str] | None
547
+ ```
__init__.py ADDED
File without changes
app.py ADDED
@@ -0,0 +1,279 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Demonstrates integrating Rerun visualization with Gradio.
3
+
4
+ Provides example implementations of data streaming, keypoint annotation, and dynamic
5
+ visualization across multiple Gradio tabs using Rerun's recording and visualization capabilities.
6
+ """
7
+
8
+ import math
9
+ import os
10
+ import tempfile
11
+ import time
12
+ import uuid
13
+
14
+ import cv2
15
+ import gradio as gr
16
+ import rerun as rr
17
+ import rerun.blueprint as rrb
18
+ from color_grid import build_color_grid
19
+ from gradio_rerun import Rerun
20
+ from gradio_rerun.events import (
21
+ SelectionChange,
22
+ TimelineChange,
23
+ TimeUpdate,
24
+ )
25
+
26
+
27
+ # Whenever we need a recording, we construct a new recording stream.
28
+ # As long as the app and recording IDs remain the same, the data
29
+ # will be merged by the Viewer.
30
+ def get_recording(recording_id: str) -> rr.RecordingStream:
31
+ return rr.RecordingStream(application_id="rerun_example_gradio", recording_id=recording_id)
32
+
33
+
34
+ # A task can directly log to a binary stream, which is routed to the embedded viewer.
35
+ # Incremental chunks are yielded to the viewer using `yield stream.read()`.
36
+ #
37
+ # This is the preferred way to work with Rerun in Gradio since your data can be immediately and
38
+ # incrementally seen by the viewer. Also, there are no ephemeral RRDs to cleanup or manage.
39
+ def streaming_repeated_blur(recording_id: str, img):
40
+ # Here we get a recording using the provided recording id.
41
+ rec = get_recording(recording_id)
42
+ stream = rec.binary_stream()
43
+
44
+ if img is None:
45
+ raise gr.Error("Must provide an image to blur.")
46
+
47
+ blueprint = rrb.Blueprint(
48
+ rrb.Horizontal(
49
+ rrb.Spatial2DView(origin="image/original"),
50
+ rrb.Spatial2DView(origin="image/blurred"),
51
+ ),
52
+ collapse_panels=True,
53
+ )
54
+
55
+ rec.send_blueprint(blueprint)
56
+ rec.set_time("iteration", sequence=0)
57
+ rec.log("image/original", rr.Image(img))
58
+ yield stream.read()
59
+
60
+ blur = img
61
+ for i in range(100):
62
+ rec.set_time("iteration", sequence=i)
63
+
64
+ # Pretend blurring takes a while so we can see streaming in action.
65
+ time.sleep(0.1)
66
+ blur = cv2.GaussianBlur(blur, (5, 5), 0)
67
+ rec.log("image/blurred", rr.Image(blur))
68
+
69
+ # Each time we yield bytes from the stream back to Gradio, they
70
+ # are incrementally sent to the viewer. Make sure to yield any time
71
+ # you want the user to be able to see progress.
72
+ yield stream.read()
73
+
74
+
75
+ # In this example the user is able to add keypoints to an image visualized in Rerun.
76
+ # These keypoints are stored in the global state, we use the session id to keep track of which keypoints belong
77
+ # to a specific session (https://www.gradio.app/guides/state-in-blocks).
78
+ #
79
+ # The current session can be obtained by adding a parameter of type `gradio.Request` to your event listener functions.
80
+ Keypoint = tuple[float, float]
81
+ keypoints_per_session_per_sequence_index: dict[str, dict[int, list[Keypoint]]] = {}
82
+
83
+
84
+ def get_keypoints_for_user_at_sequence_index(request: gr.Request, sequence: int) -> list[Keypoint]:
85
+ per_sequence = keypoints_per_session_per_sequence_index[request.session_hash]
86
+ if sequence not in per_sequence:
87
+ per_sequence[sequence] = []
88
+
89
+ return per_sequence[sequence]
90
+
91
+
92
+ def initialize_instance(request: gr.Request) -> None:
93
+ keypoints_per_session_per_sequence_index[request.session_hash] = {}
94
+
95
+
96
+ def cleanup_instance(request: gr.Request) -> None:
97
+ if request.session_hash in keypoints_per_session_per_sequence_index:
98
+ del keypoints_per_session_per_sequence_index[request.session_hash]
99
+
100
+
101
+ # In this function, the `request` and `evt` parameters will be automatically injected by Gradio when this
102
+ # event listener is fired.
103
+ #
104
+ # `SelectionChange` is a subclass of `EventData`: https://www.gradio.app/docs/gradio/eventdata
105
+ # `gr.Request`: https://www.gradio.app/main/docs/gradio/request
106
+ def register_keypoint(
107
+ active_recording_id: str,
108
+ current_timeline: str,
109
+ current_time: float,
110
+ request: gr.Request,
111
+ change: SelectionChange,
112
+ ):
113
+ if active_recording_id == "":
114
+ return
115
+
116
+ if current_timeline != "iteration":
117
+ return
118
+
119
+ evt = change.payload
120
+
121
+ # We can only log a keypoint if the user selected only a single item.
122
+ if len(evt.items) != 1:
123
+ return
124
+ item = evt.items[0]
125
+
126
+ # If the selected item isn't an entity, or we don't have its position, then bail out.
127
+ if item.type != "entity" or item.position is None:
128
+ return
129
+
130
+ # Now we can produce a valid keypoint.
131
+ rec = get_recording(active_recording_id)
132
+ stream = rec.binary_stream()
133
+
134
+ # We round `current_time` toward 0, because that gives us the sequence index
135
+ # that the user is currently looking at, due to the Viewer's latest-at semantics.
136
+ index = math.floor(current_time)
137
+
138
+ # We keep track of the keypoints per sequence index for each user manually.
139
+ keypoints = get_keypoints_for_user_at_sequence_index(request, index)
140
+ keypoints.append(item.position[0:2])
141
+
142
+ rec.set_time("iteration", sequence=index)
143
+ rec.log(f"{item.entity_path}/keypoint", rr.Points2D(keypoints, radii=2))
144
+
145
+ yield stream.read()
146
+
147
+
148
+ def track_current_time(evt: TimeUpdate):
149
+ return evt.payload.time
150
+
151
+
152
+ def track_current_timeline_and_time(evt: TimelineChange):
153
+ return evt.payload.timeline, evt.payload.time
154
+
155
+
156
+ # However, if you have a workflow that creates an RRD file instead, you can still send it
157
+ # directly to the viewer by simply returning the path to the RRD file.
158
+ #
159
+ # This may be helpful if you need to execute a helper tool written in C++ or Rust that can't
160
+ # be easily modified to stream data directly via Gradio.
161
+ #
162
+ # In this case you may want to clean up the RRD file after it's sent to the viewer so that you
163
+ # don't accumulate too many temporary files.
164
+ @rr.thread_local_stream("rerun_example_cube_rrd")
165
+ def create_cube_rrd(x, y, z, pending_cleanup):
166
+ cube = build_color_grid(int(x), int(y), int(z), twist=0)
167
+ rr.log("cube", rr.Points3D(cube.positions, colors=cube.colors, radii=0.5))
168
+
169
+ # Simulate delay
170
+ time.sleep(x / 10)
171
+
172
+ # We eventually want to clean up the RRD file after it's sent to the viewer, so tracking
173
+ # any pending files to be cleaned up when the state is deleted.
174
+ temp = tempfile.NamedTemporaryFile(prefix="cube_", suffix=".rrd", delete=False)
175
+ pending_cleanup.append(temp.name)
176
+
177
+ blueprint = rrb.Spatial3DView(origin="cube")
178
+ rr.save(temp.name, default_blueprint=blueprint)
179
+
180
+ # Just return the name of the file -- Gradio will convert it to a FileData object
181
+ # and send it to the viewer.
182
+ return temp.name
183
+
184
+
185
+ def cleanup_cube_rrds(pending_cleanup: list[str]) -> None:
186
+ for f in pending_cleanup:
187
+ os.unlink(f)
188
+
189
+
190
+ with gr.Blocks() as demo:
191
+ with gr.Tab("Streaming"):
192
+ with gr.Row():
193
+ img = gr.Image(interactive=True, label="Image")
194
+ with gr.Column():
195
+ stream_blur = gr.Button("Stream Repeated Blur")
196
+
197
+ with gr.Row():
198
+ viewer = Rerun(
199
+ streaming=True,
200
+ panel_states={
201
+ "time": "collapsed",
202
+ "blueprint": "hidden",
203
+ "selection": "hidden",
204
+ },
205
+ )
206
+
207
+ # We make a new recording id, and store it in a Gradio's session state.
208
+ recording_id = gr.State(uuid.uuid4())
209
+
210
+ # Also store the current timeline and time of the viewer in the session state.
211
+ current_timeline = gr.State("")
212
+ current_time = gr.State(0.0)
213
+
214
+ # When registering the event listeners, we pass the `recording_id` in as input in order to create
215
+ # a recording stream using that id.
216
+ stream_blur.click(
217
+ # Using the `viewer` as an output allows us to stream data to it by yielding bytes from the callback.
218
+ streaming_repeated_blur,
219
+ inputs=[recording_id, img],
220
+ outputs=[viewer],
221
+ )
222
+ viewer.selection_change(
223
+ register_keypoint,
224
+ inputs=[recording_id, current_timeline, current_time],
225
+ outputs=[viewer],
226
+ )
227
+ viewer.time_update(track_current_time, outputs=[current_time])
228
+ viewer.timeline_change(track_current_timeline_and_time, outputs=[current_timeline, current_time])
229
+ with gr.Tab("Dynamic RRD"):
230
+ pending_cleanup = gr.State([], time_to_live=10, delete_callback=cleanup_cube_rrds)
231
+ with gr.Row():
232
+ x_count = gr.Number(minimum=1, maximum=10, value=5, precision=0, label="X Count")
233
+ y_count = gr.Number(minimum=1, maximum=10, value=5, precision=0, label="Y Count")
234
+ z_count = gr.Number(minimum=1, maximum=10, value=5, precision=0, label="Z Count")
235
+ with gr.Row():
236
+ create_rrd = gr.Button("Create RRD")
237
+ with gr.Row():
238
+ viewer = Rerun(
239
+ streaming=True,
240
+ panel_states={
241
+ "time": "collapsed",
242
+ "blueprint": "hidden",
243
+ "selection": "hidden",
244
+ },
245
+ )
246
+ create_rrd.click(
247
+ create_cube_rrd,
248
+ inputs=[x_count, y_count, z_count, pending_cleanup],
249
+ outputs=[viewer],
250
+ )
251
+
252
+ with gr.Tab("Hosted RRD"):
253
+ with gr.Row():
254
+ # It may be helpful to point the viewer to a hosted RRD file on another server.
255
+ # If an RRD file is hosted via http, you can just return a URL to the file.
256
+ choose_rrd = gr.Dropdown(
257
+ label="RRD",
258
+ choices=[
259
+ f"{rr.bindings.get_app_url()}/examples/arkit_scenes.rrd",
260
+ f"{rr.bindings.get_app_url()}/examples/dna.rrd",
261
+ f"{rr.bindings.get_app_url()}/examples/plots.rrd",
262
+ ],
263
+ )
264
+ with gr.Row():
265
+ viewer = Rerun(
266
+ streaming=True,
267
+ panel_states={
268
+ "time": "collapsed",
269
+ "blueprint": "hidden",
270
+ "selection": "hidden",
271
+ },
272
+ )
273
+ choose_rrd.change(lambda x: x, inputs=[choose_rrd], outputs=[viewer])
274
+ demo.load(initialize_instance)
275
+ demo.close(cleanup_instance)
276
+
277
+
278
+ if __name__ == "__main__":
279
+ demo.launch()
color_grid.py ADDED
@@ -0,0 +1,47 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from collections import namedtuple # noqa: D100
2
+ from math import cos, sin
3
+
4
+ import numpy as np
5
+
6
+ ColorGrid = namedtuple("ColorGrid", ["positions", "colors"])
7
+
8
+
9
+ def build_color_grid(x_count: int = 10, y_count: int = 10, z_count: int = 10, twist: int = 0) -> ColorGrid:
10
+ """
11
+ Create a cube of points with colors.
12
+
13
+ The total point cloud will have x_count * y_count * z_count points.
14
+
15
+ Parameters
16
+ ----------
17
+ x_count, y_count, z_count:
18
+ Number of points in each dimension.
19
+ twist:
20
+ Angle to twist from bottom to top of the cube
21
+
22
+ """
23
+ grid = np.mgrid[
24
+ slice(-x_count, x_count, x_count * 1j),
25
+ slice(-y_count, y_count, y_count * 1j),
26
+ slice(-z_count, z_count, z_count * 1j),
27
+ ]
28
+
29
+ angle = np.linspace(-float(twist) / 2, float(twist) / 2, z_count)
30
+ for z in range(z_count):
31
+ xv, yv, zv = grid[:, :, :, z]
32
+ rot_xv = xv * cos(angle[z]) - yv * sin(angle[z])
33
+ rot_yv = xv * sin(angle[z]) + yv * cos(angle[z])
34
+ grid[:, :, :, z] = [rot_xv, rot_yv, zv]
35
+
36
+ positions = np.vstack([xyz.ravel() for xyz in grid])
37
+
38
+ colors = np.vstack([
39
+ xyz.ravel()
40
+ for xyz in np.mgrid[
41
+ slice(0, 255, x_count * 1j),
42
+ slice(0, 255, y_count * 1j),
43
+ slice(0, 255, z_count * 1j),
44
+ ]
45
+ ])
46
+
47
+ return ColorGrid(positions.T, colors.T.astype(np.uint8))
css.css ADDED
@@ -0,0 +1,157 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ html {
2
+ font-family: Inter;
3
+ font-size: 16px;
4
+ font-weight: 400;
5
+ line-height: 1.5;
6
+ -webkit-text-size-adjust: 100%;
7
+ background: #fff;
8
+ color: #323232;
9
+ -webkit-font-smoothing: antialiased;
10
+ -moz-osx-font-smoothing: grayscale;
11
+ text-rendering: optimizeLegibility;
12
+ }
13
+
14
+ :root {
15
+ --space: 1;
16
+ --vspace: calc(var(--space) * 1rem);
17
+ --vspace-0: calc(3 * var(--space) * 1rem);
18
+ --vspace-1: calc(2 * var(--space) * 1rem);
19
+ --vspace-2: calc(1.5 * var(--space) * 1rem);
20
+ --vspace-3: calc(0.5 * var(--space) * 1rem);
21
+ }
22
+
23
+ .app {
24
+ max-width: 748px !important;
25
+ }
26
+
27
+ .prose p {
28
+ margin: var(--vspace) 0;
29
+ line-height: var(--vspace * 2);
30
+ font-size: 1rem;
31
+ }
32
+
33
+ code {
34
+ font-family: "Inconsolata", sans-serif;
35
+ font-size: 16px;
36
+ }
37
+
38
+ h1,
39
+ h1 code {
40
+ font-weight: 400;
41
+ line-height: calc(2.5 / var(--space) * var(--vspace));
42
+ }
43
+
44
+ h1 code {
45
+ background: none;
46
+ border: none;
47
+ letter-spacing: 0.05em;
48
+ padding-bottom: 5px;
49
+ position: relative;
50
+ padding: 0;
51
+ }
52
+
53
+ h2 {
54
+ margin: var(--vspace-1) 0 var(--vspace-2) 0;
55
+ line-height: 1em;
56
+ }
57
+
58
+ h3,
59
+ h3 code {
60
+ margin: var(--vspace-1) 0 var(--vspace-2) 0;
61
+ line-height: 1em;
62
+ }
63
+
64
+ h4,
65
+ h5,
66
+ h6 {
67
+ margin: var(--vspace-3) 0 var(--vspace-3) 0;
68
+ line-height: var(--vspace);
69
+ }
70
+
71
+ .bigtitle,
72
+ h1,
73
+ h1 code {
74
+ font-size: calc(8px * 4.5);
75
+ word-break: break-word;
76
+ }
77
+
78
+ .title,
79
+ h2,
80
+ h2 code {
81
+ font-size: calc(8px * 3.375);
82
+ font-weight: lighter;
83
+ word-break: break-word;
84
+ border: none;
85
+ background: none;
86
+ }
87
+
88
+ .subheading1,
89
+ h3,
90
+ h3 code {
91
+ font-size: calc(8px * 1.8);
92
+ font-weight: 600;
93
+ border: none;
94
+ background: none;
95
+ letter-spacing: 0.1em;
96
+ text-transform: uppercase;
97
+ }
98
+
99
+ h2 code {
100
+ padding: 0;
101
+ position: relative;
102
+ letter-spacing: 0.05em;
103
+ }
104
+
105
+ blockquote {
106
+ font-size: calc(8px * 1.1667);
107
+ font-style: italic;
108
+ line-height: calc(1.1667 * var(--vspace));
109
+ margin: var(--vspace-2) var(--vspace-2);
110
+ }
111
+
112
+ .subheading2,
113
+ h4 {
114
+ font-size: calc(8px * 1.4292);
115
+ text-transform: uppercase;
116
+ font-weight: 600;
117
+ }
118
+
119
+ .subheading3,
120
+ h5 {
121
+ font-size: calc(8px * 1.2917);
122
+ line-height: calc(1.2917 * var(--vspace));
123
+
124
+ font-weight: lighter;
125
+ text-transform: uppercase;
126
+ letter-spacing: 0.15em;
127
+ }
128
+
129
+ h6 {
130
+ font-size: calc(8px * 1.1667);
131
+ font-size: 1.1667em;
132
+ font-weight: normal;
133
+ font-style: italic;
134
+ font-family: "le-monde-livre-classic-byol", serif !important;
135
+ letter-spacing: 0px !important;
136
+ }
137
+
138
+ #start .md > *:first-child {
139
+ margin-top: 0;
140
+ }
141
+
142
+ h2 + h3 {
143
+ margin-top: 0;
144
+ }
145
+
146
+ .md hr {
147
+ border: none;
148
+ border-top: 1px solid var(--block-border-color);
149
+ margin: var(--vspace-2) 0 var(--vspace-2) 0;
150
+ }
151
+ .prose ul {
152
+ margin: var(--vspace-2) 0 var(--vspace-1) 0;
153
+ }
154
+
155
+ .gap {
156
+ gap: 0;
157
+ }
requirements.txt ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ gradio_rerun
2
+ opencv-python
space.py ADDED
@@ -0,0 +1,409 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ import gradio as gr
3
+ from app import demo as app
4
+ import os
5
+
6
+ _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']}}}
7
+
8
+ abs_path = os.path.join(os.path.dirname(__file__), "css.css")
9
+
10
+ with gr.Blocks(
11
+ css=abs_path,
12
+ theme=gr.themes.Default(
13
+ font_mono=[
14
+ gr.themes.GoogleFont("Inconsolata"),
15
+ "monospace",
16
+ ],
17
+ ),
18
+ ) as demo:
19
+ gr.Markdown(
20
+ """
21
+ # `gradio_rerun`
22
+
23
+ <div style="display: flex; gap: 7px;">
24
+ <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>
25
+ </div>
26
+
27
+ Rerun viewer with Gradio
28
+ """, elem_classes=["md-custom"], header_links=True)
29
+ app.render()
30
+ gr.Markdown(
31
+ """
32
+ ## Installation
33
+
34
+ ```bash
35
+ pip install gradio_rerun
36
+ ```
37
+
38
+ ## Usage
39
+
40
+ ```python
41
+ \"\"\"
42
+ Demonstrates integrating Rerun visualization with Gradio.
43
+
44
+ Provides example implementations of data streaming, keypoint annotation, and dynamic
45
+ visualization across multiple Gradio tabs using Rerun's recording and visualization capabilities.
46
+ \"\"\"
47
+
48
+ import math
49
+ import os
50
+ import tempfile
51
+ import time
52
+ import uuid
53
+
54
+ import cv2
55
+ import gradio as gr
56
+ import rerun as rr
57
+ import rerun.blueprint as rrb
58
+ from color_grid import build_color_grid
59
+ from gradio_rerun import Rerun
60
+ from gradio_rerun.events import (
61
+ SelectionChange,
62
+ TimelineChange,
63
+ TimeUpdate,
64
+ )
65
+
66
+
67
+ # Whenever we need a recording, we construct a new recording stream.
68
+ # As long as the app and recording IDs remain the same, the data
69
+ # will be merged by the Viewer.
70
+ def get_recording(recording_id: str) -> rr.RecordingStream:
71
+ return rr.RecordingStream(application_id="rerun_example_gradio", recording_id=recording_id)
72
+
73
+
74
+ # A task can directly log to a binary stream, which is routed to the embedded viewer.
75
+ # Incremental chunks are yielded to the viewer using `yield stream.read()`.
76
+ #
77
+ # This is the preferred way to work with Rerun in Gradio since your data can be immediately and
78
+ # incrementally seen by the viewer. Also, there are no ephemeral RRDs to cleanup or manage.
79
+ def streaming_repeated_blur(recording_id: str, img):
80
+ # Here we get a recording using the provided recording id.
81
+ rec = get_recording(recording_id)
82
+ stream = rec.binary_stream()
83
+
84
+ if img is None:
85
+ raise gr.Error("Must provide an image to blur.")
86
+
87
+ blueprint = rrb.Blueprint(
88
+ rrb.Horizontal(
89
+ rrb.Spatial2DView(origin="image/original"),
90
+ rrb.Spatial2DView(origin="image/blurred"),
91
+ ),
92
+ collapse_panels=True,
93
+ )
94
+
95
+ rec.send_blueprint(blueprint)
96
+ rec.set_time("iteration", sequence=0)
97
+ rec.log("image/original", rr.Image(img))
98
+ yield stream.read()
99
+
100
+ blur = img
101
+ for i in range(100):
102
+ rec.set_time("iteration", sequence=i)
103
+
104
+ # Pretend blurring takes a while so we can see streaming in action.
105
+ time.sleep(0.1)
106
+ blur = cv2.GaussianBlur(blur, (5, 5), 0)
107
+ rec.log("image/blurred", rr.Image(blur))
108
+
109
+ # Each time we yield bytes from the stream back to Gradio, they
110
+ # are incrementally sent to the viewer. Make sure to yield any time
111
+ # you want the user to be able to see progress.
112
+ yield stream.read()
113
+
114
+
115
+ # In this example the user is able to add keypoints to an image visualized in Rerun.
116
+ # These keypoints are stored in the global state, we use the session id to keep track of which keypoints belong
117
+ # to a specific session (https://www.gradio.app/guides/state-in-blocks).
118
+ #
119
+ # The current session can be obtained by adding a parameter of type `gradio.Request` to your event listener functions.
120
+ Keypoint = tuple[float, float]
121
+ keypoints_per_session_per_sequence_index: dict[str, dict[int, list[Keypoint]]] = {}
122
+
123
+
124
+ def get_keypoints_for_user_at_sequence_index(request: gr.Request, sequence: int) -> list[Keypoint]:
125
+ per_sequence = keypoints_per_session_per_sequence_index[request.session_hash]
126
+ if sequence not in per_sequence:
127
+ per_sequence[sequence] = []
128
+
129
+ return per_sequence[sequence]
130
+
131
+
132
+ def initialize_instance(request: gr.Request) -> None:
133
+ keypoints_per_session_per_sequence_index[request.session_hash] = {}
134
+
135
+
136
+ def cleanup_instance(request: gr.Request) -> None:
137
+ if request.session_hash in keypoints_per_session_per_sequence_index:
138
+ del keypoints_per_session_per_sequence_index[request.session_hash]
139
+
140
+
141
+ # In this function, the `request` and `evt` parameters will be automatically injected by Gradio when this
142
+ # event listener is fired.
143
+ #
144
+ # `SelectionChange` is a subclass of `EventData`: https://www.gradio.app/docs/gradio/eventdata
145
+ # `gr.Request`: https://www.gradio.app/main/docs/gradio/request
146
+ def register_keypoint(
147
+ active_recording_id: str,
148
+ current_timeline: str,
149
+ current_time: float,
150
+ request: gr.Request,
151
+ change: SelectionChange,
152
+ ):
153
+ if active_recording_id == "":
154
+ return
155
+
156
+ if current_timeline != "iteration":
157
+ return
158
+
159
+ evt = change.payload
160
+
161
+ # We can only log a keypoint if the user selected only a single item.
162
+ if len(evt.items) != 1:
163
+ return
164
+ item = evt.items[0]
165
+
166
+ # If the selected item isn't an entity, or we don't have its position, then bail out.
167
+ if item.type != "entity" or item.position is None:
168
+ return
169
+
170
+ # Now we can produce a valid keypoint.
171
+ rec = get_recording(active_recording_id)
172
+ stream = rec.binary_stream()
173
+
174
+ # We round `current_time` toward 0, because that gives us the sequence index
175
+ # that the user is currently looking at, due to the Viewer's latest-at semantics.
176
+ index = math.floor(current_time)
177
+
178
+ # We keep track of the keypoints per sequence index for each user manually.
179
+ keypoints = get_keypoints_for_user_at_sequence_index(request, index)
180
+ keypoints.append(item.position[0:2])
181
+
182
+ rec.set_time("iteration", sequence=index)
183
+ rec.log(f"{item.entity_path}/keypoint", rr.Points2D(keypoints, radii=2))
184
+
185
+ yield stream.read()
186
+
187
+
188
+ def track_current_time(evt: TimeUpdate):
189
+ return evt.payload.time
190
+
191
+
192
+ def track_current_timeline_and_time(evt: TimelineChange):
193
+ return evt.payload.timeline, evt.payload.time
194
+
195
+
196
+ # However, if you have a workflow that creates an RRD file instead, you can still send it
197
+ # directly to the viewer by simply returning the path to the RRD file.
198
+ #
199
+ # This may be helpful if you need to execute a helper tool written in C++ or Rust that can't
200
+ # be easily modified to stream data directly via Gradio.
201
+ #
202
+ # In this case you may want to clean up the RRD file after it's sent to the viewer so that you
203
+ # don't accumulate too many temporary files.
204
+ @rr.thread_local_stream("rerun_example_cube_rrd")
205
+ def create_cube_rrd(x, y, z, pending_cleanup):
206
+ cube = build_color_grid(int(x), int(y), int(z), twist=0)
207
+ rr.log("cube", rr.Points3D(cube.positions, colors=cube.colors, radii=0.5))
208
+
209
+ # Simulate delay
210
+ time.sleep(x / 10)
211
+
212
+ # We eventually want to clean up the RRD file after it's sent to the viewer, so tracking
213
+ # any pending files to be cleaned up when the state is deleted.
214
+ temp = tempfile.NamedTemporaryFile(prefix="cube_", suffix=".rrd", delete=False)
215
+ pending_cleanup.append(temp.name)
216
+
217
+ blueprint = rrb.Spatial3DView(origin="cube")
218
+ rr.save(temp.name, default_blueprint=blueprint)
219
+
220
+ # Just return the name of the file -- Gradio will convert it to a FileData object
221
+ # and send it to the viewer.
222
+ return temp.name
223
+
224
+
225
+ def cleanup_cube_rrds(pending_cleanup: list[str]) -> None:
226
+ for f in pending_cleanup:
227
+ os.unlink(f)
228
+
229
+
230
+ with gr.Blocks() as demo:
231
+ with gr.Tab("Streaming"):
232
+ with gr.Row():
233
+ img = gr.Image(interactive=True, label="Image")
234
+ with gr.Column():
235
+ stream_blur = gr.Button("Stream Repeated Blur")
236
+
237
+ with gr.Row():
238
+ viewer = Rerun(
239
+ streaming=True,
240
+ panel_states={
241
+ "time": "collapsed",
242
+ "blueprint": "hidden",
243
+ "selection": "hidden",
244
+ },
245
+ )
246
+
247
+ # We make a new recording id, and store it in a Gradio's session state.
248
+ recording_id = gr.State(uuid.uuid4())
249
+
250
+ # Also store the current timeline and time of the viewer in the session state.
251
+ current_timeline = gr.State("")
252
+ current_time = gr.State(0.0)
253
+
254
+ # When registering the event listeners, we pass the `recording_id` in as input in order to create
255
+ # a recording stream using that id.
256
+ stream_blur.click(
257
+ # Using the `viewer` as an output allows us to stream data to it by yielding bytes from the callback.
258
+ streaming_repeated_blur,
259
+ inputs=[recording_id, img],
260
+ outputs=[viewer],
261
+ )
262
+ viewer.selection_change(
263
+ register_keypoint,
264
+ inputs=[recording_id, current_timeline, current_time],
265
+ outputs=[viewer],
266
+ )
267
+ viewer.time_update(track_current_time, outputs=[current_time])
268
+ viewer.timeline_change(track_current_timeline_and_time, outputs=[current_timeline, current_time])
269
+ with gr.Tab("Dynamic RRD"):
270
+ pending_cleanup = gr.State([], time_to_live=10, delete_callback=cleanup_cube_rrds)
271
+ with gr.Row():
272
+ x_count = gr.Number(minimum=1, maximum=10, value=5, precision=0, label="X Count")
273
+ y_count = gr.Number(minimum=1, maximum=10, value=5, precision=0, label="Y Count")
274
+ z_count = gr.Number(minimum=1, maximum=10, value=5, precision=0, label="Z Count")
275
+ with gr.Row():
276
+ create_rrd = gr.Button("Create RRD")
277
+ with gr.Row():
278
+ viewer = Rerun(
279
+ streaming=True,
280
+ panel_states={
281
+ "time": "collapsed",
282
+ "blueprint": "hidden",
283
+ "selection": "hidden",
284
+ },
285
+ )
286
+ create_rrd.click(
287
+ create_cube_rrd,
288
+ inputs=[x_count, y_count, z_count, pending_cleanup],
289
+ outputs=[viewer],
290
+ )
291
+
292
+ with gr.Tab("Hosted RRD"):
293
+ with gr.Row():
294
+ # It may be helpful to point the viewer to a hosted RRD file on another server.
295
+ # If an RRD file is hosted via http, you can just return a URL to the file.
296
+ choose_rrd = gr.Dropdown(
297
+ label="RRD",
298
+ choices=[
299
+ f"{rr.bindings.get_app_url()}/examples/arkit_scenes.rrd",
300
+ f"{rr.bindings.get_app_url()}/examples/dna.rrd",
301
+ f"{rr.bindings.get_app_url()}/examples/plots.rrd",
302
+ ],
303
+ )
304
+ with gr.Row():
305
+ viewer = Rerun(
306
+ streaming=True,
307
+ panel_states={
308
+ "time": "collapsed",
309
+ "blueprint": "hidden",
310
+ "selection": "hidden",
311
+ },
312
+ )
313
+ choose_rrd.change(lambda x: x, inputs=[choose_rrd], outputs=[viewer])
314
+ demo.load(initialize_instance)
315
+ demo.close(cleanup_instance)
316
+
317
+
318
+ if __name__ == "__main__":
319
+ demo.launch()
320
+
321
+ ```
322
+ """, elem_classes=["md-custom"], header_links=True)
323
+
324
+
325
+ gr.Markdown("""
326
+ ## `Rerun`
327
+
328
+ ### Initialization
329
+ """, elem_classes=["md-custom"], header_links=True)
330
+
331
+ gr.ParamViewer(value=_docs["Rerun"]["members"]["__init__"], linkify=['RerunData'])
332
+
333
+
334
+ gr.Markdown("### Events")
335
+ gr.ParamViewer(value=_docs["Rerun"]["events"], linkify=['Event'])
336
+
337
+
338
+
339
+
340
+ gr.Markdown("""
341
+
342
+ ### User function
343
+
344
+ 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).
345
+
346
+ - When used as an Input, the component only impacts the input signature of the user function.
347
+ - When used as an output, the component only impacts the return signature of the user function.
348
+
349
+ The code snippet below is accurate in cases where the component is used as both an input and an output.
350
+
351
+ - **As input:** Is passed, a `RerunData` object.
352
+ - **As output:** Should return, the value to send over to the Rerun viewer on the front-end.
353
+
354
+ ```python
355
+ def predict(
356
+ value: RerunData | None
357
+ ) -> list[pathlib.Path | str] | pathlib.Path | str | bytes:
358
+ return value
359
+ ```
360
+ """, elem_classes=["md-custom", "Rerun-user-fn"], header_links=True)
361
+
362
+
363
+
364
+
365
+ code_RerunData = gr.Markdown("""
366
+ ## `RerunData`
367
+ ```python
368
+ class RerunData(GradioRootModel):
369
+ root: Sequence[FileData | Path | str] | None
370
+ ```""", elem_classes=["md-custom", "RerunData"], header_links=True)
371
+
372
+ demo.load(None, js=r"""function() {
373
+ const refs = {
374
+ RerunData: [], };
375
+ const user_fn_refs = {
376
+ Rerun: ['RerunData'], };
377
+ requestAnimationFrame(() => {
378
+
379
+ Object.entries(user_fn_refs).forEach(([key, refs]) => {
380
+ if (refs.length > 0) {
381
+ const el = document.querySelector(`.${key}-user-fn`);
382
+ if (!el) return;
383
+ refs.forEach(ref => {
384
+ el.innerHTML = el.innerHTML.replace(
385
+ new RegExp("\\b"+ref+"\\b", "g"),
386
+ `<a href="#h-${ref.toLowerCase()}">${ref}</a>`
387
+ );
388
+ })
389
+ }
390
+ })
391
+
392
+ Object.entries(refs).forEach(([key, refs]) => {
393
+ if (refs.length > 0) {
394
+ const el = document.querySelector(`.${key}`);
395
+ if (!el) return;
396
+ refs.forEach(ref => {
397
+ el.innerHTML = el.innerHTML.replace(
398
+ new RegExp("\\b"+ref+"\\b", "g"),
399
+ `<a href="#h-${ref.toLowerCase()}">${ref}</a>`
400
+ );
401
+ })
402
+ }
403
+ })
404
+ })
405
+ }
406
+
407
+ """)
408
+
409
+ demo.launch()