smileyenot983 commited on
Commit
771e891
·
verified ·
1 Parent(s): 7496909

Upload folder using huggingface_hub

Browse files
CHANGELOG.md CHANGED
@@ -1,5 +1,40 @@
1
  # trackio
2
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3
  ## 0.7.0
4
 
5
  ### Features
 
1
  # trackio
2
 
3
+ ## 0.10.0
4
+
5
+ ### Features
6
+
7
+ - [#305](https://github.com/gradio-app/trackio/pull/305) [`e64883a`](https://github.com/gradio-app/trackio/commit/e64883a51f7b8b93f7d48b8afe55acdb62238b71) - bump to gradio 6.0, make `trackio` compatible, and fix related issues. Thanks @abidlabs!
8
+
9
+ ## 0.9.1
10
+
11
+ ### Features
12
+
13
+ - [#344](https://github.com/gradio-app/trackio/pull/344) [`7e01024`](https://github.com/gradio-app/trackio/commit/7e010241d9a34794e0ce0dc19c1a6f0cf94ba856) - Avoid redundant calls to /whoami-v2. Thanks @Wauplin!
14
+
15
+ ## 0.9.0
16
+
17
+ ### Features
18
+
19
+ - [#343](https://github.com/gradio-app/trackio/pull/343) [`51bea30`](https://github.com/gradio-app/trackio/commit/51bea30f2877adff8e6497466d3a799400a0a049) - Sync offline projects to Hugging Face spaces. Thanks @candemircan!
20
+ - [#341](https://github.com/gradio-app/trackio/pull/341) [`4fd841f`](https://github.com/gradio-app/trackio/commit/4fd841fa190e15071b02f6fba7683ef4f393a654) - Adds a basic UI test to `trackio`. Thanks @abidlabs!
21
+ - [#339](https://github.com/gradio-app/trackio/pull/339) [`011d91b`](https://github.com/gradio-app/trackio/commit/011d91bb6ae266516fd250a349285670a8049d05) - Allow customzing the trackio color palette. Thanks @abidlabs!
22
+
23
+ ## 0.8.1
24
+
25
+ ### Features
26
+
27
+ - [#336](https://github.com/gradio-app/trackio/pull/336) [`5f9f51d`](https://github.com/gradio-app/trackio/commit/5f9f51dac8677f240d7c42c3e3b2660a22aee138) - Support a list of `Trackio.Image` in a `trackio.Table` cell. Thanks @abidlabs!
28
+
29
+ ## 0.8.0
30
+
31
+ ### Features
32
+
33
+ - [#331](https://github.com/gradio-app/trackio/pull/331) [`2c02d0f`](https://github.com/gradio-app/trackio/commit/2c02d0fd0a5824160528782402bb0dd4083396d5) - Truncate table string values that are greater than 250 characters (configuirable via env variable). Thanks @abidlabs!
34
+ - [#324](https://github.com/gradio-app/trackio/pull/324) [`50b2122`](https://github.com/gradio-app/trackio/commit/50b2122e7965ac82a72e6cb3b7d048bc10a2a6b1) - Add log y-axis functionality to UI. Thanks @abidlabs!
35
+ - [#326](https://github.com/gradio-app/trackio/pull/326) [`61dc1f4`](https://github.com/gradio-app/trackio/commit/61dc1f40af2f545f8e70395ddf0dbb8aee6b60d5) - Fix: improve table rendering for metrics in Trackio Dashboard. Thanks @vigneshwaran!
36
+ - [#328](https://github.com/gradio-app/trackio/pull/328) [`6857cbb`](https://github.com/gradio-app/trackio/commit/6857cbbe557a59a4642f210ec42566d108294e63) - Support trackio.Table with trackio.Image columns. Thanks @abidlabs!
37
+
38
  ## 0.7.0
39
 
40
  ### Features
__init__.py CHANGED
@@ -1,4 +1,3 @@
1
- import hashlib
2
  import json
3
  import logging
4
  import os
@@ -7,20 +6,20 @@ import webbrowser
7
  from pathlib import Path
8
  from typing import Any
9
 
10
- from gradio.blocks import BUILT_IN_THEMES
11
- from gradio.themes import Default as DefaultTheme
12
  from gradio.themes import ThemeClass
 
13
  from gradio_client import Client
14
  from huggingface_hub import SpaceStorage
15
 
16
  from trackio import context_vars, deploy, utils
 
17
  from trackio.histogram import Histogram
18
  from trackio.imports import import_csv, import_tf_events
19
  from trackio.media import TrackioAudio, TrackioImage, TrackioVideo
20
  from trackio.run import Run
21
  from trackio.sqlite_storage import SQLiteStorage
22
  from trackio.table import Table
23
- from trackio.ui.main import demo
24
  from trackio.utils import TRACKIO_DIR, TRACKIO_LOGO_DIR
25
 
26
  logging.getLogger("httpx").setLevel(logging.WARNING)
@@ -41,6 +40,8 @@ __all__ = [
41
  "log",
42
  "finish",
43
  "show",
 
 
44
  "import_csv",
45
  "import_tf_events",
46
  "Image",
@@ -140,7 +141,9 @@ def init(
140
  if url is None:
141
  if space_id is None:
142
  _, url, share_url = demo.launch(
143
- show_api=False,
 
 
144
  inline=False,
145
  quiet=True,
146
  prevent_thread_lock=True,
@@ -169,7 +172,7 @@ def init(
169
  if utils.is_in_notebook() and embed:
170
  base_url = share_url + "/" if share_url else url
171
  full_url = utils.get_full_url(
172
- base_url, project=project, write_token=demo.write_token
173
  )
174
  utils.embed_url_in_notebook(full_url)
175
  else:
@@ -261,10 +264,59 @@ def finish():
261
  run.finish()
262
 
263
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
264
  def show(
265
  project: str | None = None,
 
266
  theme: str | ThemeClass | None = None,
267
  mcp_server: bool | None = None,
 
 
 
 
268
  ):
269
  """
270
  Launches the Trackio dashboard.
@@ -284,31 +336,32 @@ def show(
284
  functions will be added as MCP tools. If `None` (default behavior), then the
285
  `GRADIO_MCP_SERVER` environment variable will be used to determine if the
286
  MCP server should be enabled (which is `"True"` on Hugging Face Spaces).
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
287
  """
288
- theme = theme or os.environ.get("TRACKIO_THEME", DEFAULT_THEME)
 
289
 
290
- if theme != DEFAULT_THEME:
291
- # TODO: It's a little hacky to reproduce this theme-setting logic from Gradio Blocks,
292
- # but in Gradio 6.0, the theme will be set in `launch()` instead, which means that we
293
- # will be able to remove this code.
294
- if isinstance(theme, str):
295
- if theme.lower() in BUILT_IN_THEMES:
296
- theme = BUILT_IN_THEMES[theme.lower()]
297
- else:
298
- try:
299
- theme = ThemeClass.from_hub(theme)
300
- except Exception as e:
301
- warnings.warn(f"Cannot load {theme}. Caught Exception: {str(e)}")
302
- theme = DefaultTheme()
303
- if not isinstance(theme, ThemeClass):
304
- warnings.warn("Theme should be a class loaded from gradio.themes")
305
- theme = DefaultTheme()
306
- demo.theme: ThemeClass = theme
307
- demo.theme_css = theme._get_theme_css()
308
- demo.stylesheets = theme._stylesheets
309
- theme_hasher = hashlib.sha256()
310
- theme_hasher.update(demo.theme_css.encode("utf-8"))
311
- demo.theme_hash = theme_hasher.hexdigest()
312
 
313
  _mcp_server = (
314
  mcp_server
@@ -316,24 +369,33 @@ def show(
316
  else os.environ.get("GRADIO_MCP_SERVER", "False") == "True"
317
  )
318
 
319
- _, url, share_url = demo.launch(
320
- show_api=_mcp_server,
 
 
321
  quiet=True,
322
  inline=False,
323
  prevent_thread_lock=True,
324
  favicon_path=TRACKIO_LOGO_DIR / "trackio_logo_light.png",
325
  allowed_paths=[TRACKIO_LOGO_DIR, TRACKIO_DIR],
326
  mcp_server=_mcp_server,
 
327
  )
328
 
329
  base_url = share_url + "/" if share_url else url
330
  full_url = utils.get_full_url(
331
- base_url, project=project, write_token=demo.write_token
332
  )
333
 
334
  if not utils.is_in_notebook():
335
  print(f"* Trackio UI launched at: {full_url}")
336
- webbrowser.open(full_url)
337
- utils.block_main_thread_until_keyboard_interrupt()
 
338
  else:
339
  utils.embed_url_in_notebook(full_url)
 
 
 
 
 
 
 
1
  import json
2
  import logging
3
  import os
 
6
  from pathlib import Path
7
  from typing import Any
8
 
 
 
9
  from gradio.themes import ThemeClass
10
+ from gradio.utils import TupleNoPrint
11
  from gradio_client import Client
12
  from huggingface_hub import SpaceStorage
13
 
14
  from trackio import context_vars, deploy, utils
15
+ from trackio.deploy import sync
16
  from trackio.histogram import Histogram
17
  from trackio.imports import import_csv, import_tf_events
18
  from trackio.media import TrackioAudio, TrackioImage, TrackioVideo
19
  from trackio.run import Run
20
  from trackio.sqlite_storage import SQLiteStorage
21
  from trackio.table import Table
22
+ from trackio.ui.main import CSS, HEAD, demo
23
  from trackio.utils import TRACKIO_DIR, TRACKIO_LOGO_DIR
24
 
25
  logging.getLogger("httpx").setLevel(logging.WARNING)
 
40
  "log",
41
  "finish",
42
  "show",
43
+ "sync",
44
+ "delete_project",
45
  "import_csv",
46
  "import_tf_events",
47
  "Image",
 
141
  if url is None:
142
  if space_id is None:
143
  _, url, share_url = demo.launch(
144
+ css=CSS,
145
+ head=HEAD,
146
+ footer_links=["gradio", "settings"],
147
  inline=False,
148
  quiet=True,
149
  prevent_thread_lock=True,
 
172
  if utils.is_in_notebook() and embed:
173
  base_url = share_url + "/" if share_url else url
174
  full_url = utils.get_full_url(
175
+ base_url, project=project, write_token=demo.write_token, footer=True
176
  )
177
  utils.embed_url_in_notebook(full_url)
178
  else:
 
264
  run.finish()
265
 
266
 
267
+ def delete_project(project: str, force: bool = False) -> bool:
268
+ """
269
+ Deletes a project by removing its local SQLite database.
270
+
271
+ Args:
272
+ project (`str`):
273
+ The name of the project to delete.
274
+ force (`bool`, *optional*, defaults to `False`):
275
+ If `True`, deletes the project without prompting for confirmation.
276
+ If `False`, prompts the user to confirm before deleting.
277
+
278
+ Returns:
279
+ `bool`: `True` if the project was deleted, `False` otherwise.
280
+ """
281
+ db_path = SQLiteStorage.get_project_db_path(project)
282
+
283
+ if not db_path.exists():
284
+ print(f"* Project '{project}' does not exist.")
285
+ return False
286
+
287
+ if not force:
288
+ response = input(
289
+ f"Are you sure you want to delete project '{project}'? "
290
+ f"This will permanently delete all runs and metrics. (y/N): "
291
+ )
292
+ if response.lower() not in ["y", "yes"]:
293
+ print("* Deletion cancelled.")
294
+ return False
295
+
296
+ try:
297
+ db_path.unlink()
298
+
299
+ for suffix in ("-wal", "-shm"):
300
+ sidecar = Path(str(db_path) + suffix)
301
+ if sidecar.exists():
302
+ sidecar.unlink()
303
+
304
+ print(f"* Project '{project}' has been deleted.")
305
+ return True
306
+ except Exception as e:
307
+ print(f"* Error deleting project '{project}': {e}")
308
+ return False
309
+
310
+
311
  def show(
312
  project: str | None = None,
313
+ *,
314
  theme: str | ThemeClass | None = None,
315
  mcp_server: bool | None = None,
316
+ footer: bool = True,
317
+ color_palette: list[str] | None = None,
318
+ open_browser: bool = True,
319
+ block_thread: bool | None = None,
320
  ):
321
  """
322
  Launches the Trackio dashboard.
 
336
  functions will be added as MCP tools. If `None` (default behavior), then the
337
  `GRADIO_MCP_SERVER` environment variable will be used to determine if the
338
  MCP server should be enabled (which is `"True"` on Hugging Face Spaces).
339
+ footer (`bool`, *optional*, defaults to `True`):
340
+ Whether to show the Gradio footer. When `False`, the footer will be hidden.
341
+ This can also be controlled via the `footer` query parameter in the URL.
342
+ color_palette (`list[str]`, *optional*):
343
+ A list of hex color codes to use for plot lines. If not provided, the
344
+ `TRACKIO_COLOR_PALETTE` environment variable will be used (comma-separated
345
+ hex codes), or if that is not set, the default color palette will be used.
346
+ Example: `['#FF0000', '#00FF00', '#0000FF']`
347
+ open_browser (`bool`, *optional*, defaults to `True`):
348
+ If `True` and not in a notebook, a new browser tab will be opened with the dashboard.
349
+ If `False`, the browser will not be opened.
350
+ block_thread (`bool`, *optional*):
351
+ If `True`, the main thread will be blocked until the dashboard is closed.
352
+ If `None` (default behavior), then the main thread will not be blocked if the
353
+ dashboard is launched in a notebook, otherwise the main thread will be blocked.
354
+
355
+ Returns:
356
+ `app`: The Gradio app object corresponding to the dashboard launched by Trackio.
357
+ `url`: The local URL of the dashboard.
358
+ `share_url`: The public share URL of the dashboard.
359
+ `full_url`: The full URL of the dashboard including the write token (will use the public share URL if launched publicly, otherwise the local URL).
360
  """
361
+ if color_palette is not None:
362
+ os.environ["TRACKIO_COLOR_PALETTE"] = ",".join(color_palette)
363
 
364
+ theme = theme or os.environ.get("TRACKIO_THEME", DEFAULT_THEME)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
365
 
366
  _mcp_server = (
367
  mcp_server
 
369
  else os.environ.get("GRADIO_MCP_SERVER", "False") == "True"
370
  )
371
 
372
+ app, url, share_url = demo.launch(
373
+ css=CSS,
374
+ head=HEAD,
375
+ footer_links=["gradio", "settings"] + (["api"] if _mcp_server else []),
376
  quiet=True,
377
  inline=False,
378
  prevent_thread_lock=True,
379
  favicon_path=TRACKIO_LOGO_DIR / "trackio_logo_light.png",
380
  allowed_paths=[TRACKIO_LOGO_DIR, TRACKIO_DIR],
381
  mcp_server=_mcp_server,
382
+ theme=theme,
383
  )
384
 
385
  base_url = share_url + "/" if share_url else url
386
  full_url = utils.get_full_url(
387
+ base_url, project=project, write_token=demo.write_token, footer=footer
388
  )
389
 
390
  if not utils.is_in_notebook():
391
  print(f"* Trackio UI launched at: {full_url}")
392
+ if open_browser:
393
+ webbrowser.open(full_url)
394
+ block_thread = block_thread if block_thread is not None else True
395
  else:
396
  utils.embed_url_in_notebook(full_url)
397
+ block_thread = block_thread if block_thread is not None else False
398
+
399
+ if block_thread:
400
+ utils.block_main_thread_until_keyboard_interrupt()
401
+ return TupleNoPrint((demo, url, share_url, full_url))
__pycache__/__init__.cpython-310.pyc CHANGED
Binary files a/__pycache__/__init__.cpython-310.pyc and b/__pycache__/__init__.cpython-310.pyc differ
 
__pycache__/cli.cpython-310.pyc CHANGED
Binary files a/__pycache__/cli.cpython-310.pyc and b/__pycache__/cli.cpython-310.pyc differ
 
__pycache__/commit_scheduler.cpython-310.pyc CHANGED
Binary files a/__pycache__/commit_scheduler.cpython-310.pyc and b/__pycache__/commit_scheduler.cpython-310.pyc differ
 
__pycache__/context_vars.cpython-310.pyc CHANGED
Binary files a/__pycache__/context_vars.cpython-310.pyc and b/__pycache__/context_vars.cpython-310.pyc differ
 
__pycache__/deploy.cpython-310.pyc CHANGED
Binary files a/__pycache__/deploy.cpython-310.pyc and b/__pycache__/deploy.cpython-310.pyc differ
 
__pycache__/dummy_commit_scheduler.cpython-310.pyc CHANGED
Binary files a/__pycache__/dummy_commit_scheduler.cpython-310.pyc and b/__pycache__/dummy_commit_scheduler.cpython-310.pyc differ
 
__pycache__/histogram.cpython-310.pyc CHANGED
Binary files a/__pycache__/histogram.cpython-310.pyc and b/__pycache__/histogram.cpython-310.pyc differ
 
__pycache__/imports.cpython-310.pyc CHANGED
Binary files a/__pycache__/imports.cpython-310.pyc and b/__pycache__/imports.cpython-310.pyc differ
 
__pycache__/run.cpython-310.pyc CHANGED
Binary files a/__pycache__/run.cpython-310.pyc and b/__pycache__/run.cpython-310.pyc differ
 
__pycache__/sqlite_storage.cpython-310.pyc CHANGED
Binary files a/__pycache__/sqlite_storage.cpython-310.pyc and b/__pycache__/sqlite_storage.cpython-310.pyc differ
 
__pycache__/table.cpython-310.pyc CHANGED
Binary files a/__pycache__/table.cpython-310.pyc and b/__pycache__/table.cpython-310.pyc differ
 
__pycache__/typehints.cpython-310.pyc CHANGED
Binary files a/__pycache__/typehints.cpython-310.pyc and b/__pycache__/typehints.cpython-310.pyc differ
 
__pycache__/utils.cpython-310.pyc CHANGED
Binary files a/__pycache__/utils.cpython-310.pyc and b/__pycache__/utils.cpython-310.pyc differ
 
cli.py CHANGED
@@ -1,6 +1,6 @@
1
  import argparse
2
 
3
- from trackio import show
4
 
5
 
6
  def main():
@@ -24,11 +24,67 @@ def main():
24
  action="store_true",
25
  help="Enable MCP server functionality. The Trackio dashboard will be set up as an MCP server and certain functions will be exposed as MCP tools.",
26
  )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
27
 
28
  args = parser.parse_args()
29
 
30
  if args.command == "show":
31
- show(args.project, args.theme, args.mcp_server)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
32
  else:
33
  parser.print_help()
34
 
 
1
  import argparse
2
 
3
+ from trackio import show, sync
4
 
5
 
6
  def main():
 
24
  action="store_true",
25
  help="Enable MCP server functionality. The Trackio dashboard will be set up as an MCP server and certain functions will be exposed as MCP tools.",
26
  )
27
+ ui_parser.add_argument(
28
+ "--footer",
29
+ action="store_true",
30
+ default=True,
31
+ help="Show the Gradio footer. Use --no-footer to hide it.",
32
+ )
33
+ ui_parser.add_argument(
34
+ "--no-footer",
35
+ dest="footer",
36
+ action="store_false",
37
+ help="Hide the Gradio footer.",
38
+ )
39
+ ui_parser.add_argument(
40
+ "--color-palette",
41
+ required=False,
42
+ help="Comma-separated list of hex color codes for plot lines (e.g. '#FF0000,#00FF00,#0000FF'). If not provided, the TRACKIO_COLOR_PALETTE environment variable will be used, or the default palette if not set.",
43
+ )
44
+
45
+ sync_parser = subparsers.add_parser(
46
+ "sync",
47
+ help="Sync a local project's database to a Hugging Face Space. If the Space does not exist, it will be created.",
48
+ )
49
+ sync_parser.add_argument(
50
+ "--project", required=True, help="The name of the local project."
51
+ )
52
+ sync_parser.add_argument(
53
+ "--space-id",
54
+ required=True,
55
+ help="The Hugging Face Space ID where the project will be synced (e.g. username/space_id).",
56
+ )
57
+ sync_parser.add_argument(
58
+ "--private",
59
+ action="store_true",
60
+ help="Make the Hugging Face Space private if creating a new Space. By default, the repo will be public unless the organization's default is private. This value is ignored if the repo already exists.",
61
+ )
62
+ sync_parser.add_argument(
63
+ "--force",
64
+ action="store_true",
65
+ help="Overwrite the existing database without prompting for confirmation.",
66
+ )
67
 
68
  args = parser.parse_args()
69
 
70
  if args.command == "show":
71
+ color_palette = None
72
+ if args.color_palette:
73
+ color_palette = [color.strip() for color in args.color_palette.split(",")]
74
+ show(
75
+ project=args.project,
76
+ theme=args.theme,
77
+ mcp_server=args.mcp_server,
78
+ footer=args.footer,
79
+ color_palette=color_palette,
80
+ )
81
+ elif args.command == "sync":
82
+ sync(
83
+ project=args.project,
84
+ space_id=args.space_id,
85
+ private=args.private,
86
+ force=args.force,
87
+ )
88
  else:
89
  parser.print_help()
90
 
deploy.py CHANGED
@@ -14,6 +14,7 @@ from requests import HTTPError
14
 
15
  import trackio
16
  from trackio.sqlite_storage import SQLiteStorage
 
17
 
18
  SPACE_HOST_URL = "https://{user_name}-{space_name}.hf.space/"
19
  SPACE_URL = "https://huggingface.co/spaces/{space_id}"
@@ -154,6 +155,8 @@ trackio.show()"""
154
  if theme := os.environ.get("TRACKIO_THEME"):
155
  huggingface_hub.add_space_variable(space_id, "TRACKIO_THEME", theme)
156
 
 
 
157
 
158
  def create_space_if_not_exists(
159
  space_id: str,
@@ -229,30 +232,75 @@ def wait_until_space_exists(
229
  Args:
230
  space_id: The ID of the Space to wait for.
231
  """
 
232
  delay = 1
233
- for _ in range(10):
234
  try:
235
- Client(space_id, verbose=False)
236
  return
237
- except (ReadTimeout, ValueError):
238
  time.sleep(delay)
239
- delay = min(delay * 2, 30)
240
  raise TimeoutError("Waiting for space to exist took longer than expected")
241
 
242
 
243
- def upload_db_to_space(project: str, space_id: str) -> None:
244
  """
245
- Uploads the database of a local Trackio project to a Hugging Face Space.
 
 
246
 
247
  Args:
248
  project: The name of the project to upload.
249
  space_id: The ID of the Space to upload to.
 
250
  """
251
  db_path = SQLiteStorage.get_project_db_path(project)
252
- client = Client(space_id, verbose=False)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
253
  client.predict(
254
  api_name="/upload_db_to_space",
255
  project=project,
256
  uploaded_db=handle_file(db_path),
257
  hf_token=huggingface_hub.utils.get_token(),
258
  )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
14
 
15
  import trackio
16
  from trackio.sqlite_storage import SQLiteStorage
17
+ from trackio.utils import preprocess_space_and_dataset_ids
18
 
19
  SPACE_HOST_URL = "https://{user_name}-{space_name}.hf.space/"
20
  SPACE_URL = "https://huggingface.co/spaces/{space_id}"
 
155
  if theme := os.environ.get("TRACKIO_THEME"):
156
  huggingface_hub.add_space_variable(space_id, "TRACKIO_THEME", theme)
157
 
158
+ huggingface_hub.add_space_variable(space_id, "GRADIO_MCP_SERVER", "True")
159
+
160
 
161
  def create_space_if_not_exists(
162
  space_id: str,
 
232
  Args:
233
  space_id: The ID of the Space to wait for.
234
  """
235
+ hf_api = huggingface_hub.HfApi()
236
  delay = 1
237
+ for _ in range(30):
238
  try:
239
+ hf_api.space_info(space_id)
240
  return
241
+ except (huggingface_hub.utils.HfHubHTTPError, ReadTimeout):
242
  time.sleep(delay)
243
+ delay = min(delay * 2, 60)
244
  raise TimeoutError("Waiting for space to exist took longer than expected")
245
 
246
 
247
+ def upload_db_to_space(project: str, space_id: str, force: bool = False) -> None:
248
  """
249
+ Uploads the database of a local Trackio project to a Hugging Face Space. It
250
+ uses the Gradio Client to upload since we do not want to trigger a new build
251
+ of the Space, which would happen if we used `huggingface_hub.upload_file`.
252
 
253
  Args:
254
  project: The name of the project to upload.
255
  space_id: The ID of the Space to upload to.
256
+ force: If True, overwrite existing database without prompting. If False, prompt for confirmation.
257
  """
258
  db_path = SQLiteStorage.get_project_db_path(project)
259
+ client = Client(space_id, verbose=False, httpx_kwargs={"timeout": 90})
260
+
261
+ if not force:
262
+ try:
263
+ existing_projects = client.predict(api_name="/get_all_projects")
264
+ if project in existing_projects:
265
+ response = input(
266
+ f"Database for project '{project}' already exists on Space '{space_id}'. "
267
+ f"Overwrite it? (y/N): "
268
+ )
269
+ if response.lower() not in ["y", "yes"]:
270
+ print("* Upload cancelled.")
271
+ return
272
+ except Exception as e:
273
+ print(f"* Warning: Could not check if project exists on Space: {e}")
274
+ print("* Proceeding with upload...")
275
+
276
  client.predict(
277
  api_name="/upload_db_to_space",
278
  project=project,
279
  uploaded_db=handle_file(db_path),
280
  hf_token=huggingface_hub.utils.get_token(),
281
  )
282
+
283
+
284
+ def sync(
285
+ project: str, space_id: str, private: bool | None = None, force: bool = False
286
+ ) -> None:
287
+ """
288
+ Syncs a local Trackio project's database to a Hugging Face Space.
289
+ If the Space does not exist, it will be created.
290
+
291
+ Args:
292
+ project (`str`): The name of the project to upload.
293
+ space_id (`str`): The ID of the Space to upload to (e.g., `"username/space_id"`).
294
+ private (`bool`, *optional*):
295
+ Whether to make the Space private. If None (default), the repo will be
296
+ public unless the organization's default is private. This value is ignored
297
+ if the repo already exists.
298
+ force (`bool`, *optional*, defaults to `False`):
299
+ If `True`, overwrite the existing database without prompting for confirmation.
300
+ If `False`, prompt the user before overwriting an existing database.
301
+ """
302
+ space_id, _ = preprocess_space_and_dataset_ids(space_id, None)
303
+ create_space_if_not_exists(space_id, private=private)
304
+ wait_until_space_exists(space_id)
305
+ upload_db_to_space(project, space_id, force=force)
306
+ print(f"Synced successfully to space: {SPACE_URL.format(space_id=space_id)}")
imports.py CHANGED
@@ -14,6 +14,7 @@ def import_csv(
14
  space_id: str | None = None,
15
  dataset_id: str | None = None,
16
  private: bool | None = None,
 
17
  ) -> None:
18
  """
19
  Imports a CSV file into a Trackio project. The CSV file must contain a `"step"`
@@ -143,7 +144,7 @@ def import_csv(
143
  space_id=space_id, dataset_id=dataset_id, private=private
144
  )
145
  deploy.wait_until_space_exists(space_id=space_id)
146
- deploy.upload_db_to_space(project=project, space_id=space_id)
147
  print(
148
  f"* View dashboard by going to: {deploy.SPACE_URL.format(space_id=space_id)}"
149
  )
@@ -156,6 +157,7 @@ def import_tf_events(
156
  space_id: str | None = None,
157
  dataset_id: str | None = None,
158
  private: bool | None = None,
 
159
  ) -> None:
160
  """
161
  Imports TensorFlow Events files from a directory into a Trackio project. Each
@@ -296,7 +298,7 @@ def import_tf_events(
296
  space_id, dataset_id=dataset_id, private=private
297
  )
298
  deploy.wait_until_space_exists(space_id)
299
- deploy.upload_db_to_space(project, space_id)
300
  print(
301
  f"* View dashboard by going to: {deploy.SPACE_URL.format(space_id=space_id)}"
302
  )
 
14
  space_id: str | None = None,
15
  dataset_id: str | None = None,
16
  private: bool | None = None,
17
+ force: bool = False,
18
  ) -> None:
19
  """
20
  Imports a CSV file into a Trackio project. The CSV file must contain a `"step"`
 
144
  space_id=space_id, dataset_id=dataset_id, private=private
145
  )
146
  deploy.wait_until_space_exists(space_id=space_id)
147
+ deploy.upload_db_to_space(project=project, space_id=space_id, force=force)
148
  print(
149
  f"* View dashboard by going to: {deploy.SPACE_URL.format(space_id=space_id)}"
150
  )
 
157
  space_id: str | None = None,
158
  dataset_id: str | None = None,
159
  private: bool | None = None,
160
+ force: bool = False,
161
  ) -> None:
162
  """
163
  Imports TensorFlow Events files from a directory into a Trackio project. Each
 
298
  space_id, dataset_id=dataset_id, private=private
299
  )
300
  deploy.wait_until_space_exists(space_id)
301
+ deploy.upload_db_to_space(project, space_id, force=force)
302
  print(
303
  f"* View dashboard by going to: {deploy.SPACE_URL.format(space_id=space_id)}"
304
  )
media/__pycache__/__init__.cpython-310.pyc CHANGED
Binary files a/media/__pycache__/__init__.cpython-310.pyc and b/media/__pycache__/__init__.cpython-310.pyc differ
 
media/__pycache__/audio_writer.cpython-310.pyc CHANGED
Binary files a/media/__pycache__/audio_writer.cpython-310.pyc and b/media/__pycache__/audio_writer.cpython-310.pyc differ
 
media/__pycache__/file_storage.cpython-310.pyc CHANGED
Binary files a/media/__pycache__/file_storage.cpython-310.pyc and b/media/__pycache__/file_storage.cpython-310.pyc differ
 
media/__pycache__/media.cpython-310.pyc CHANGED
Binary files a/media/__pycache__/media.cpython-310.pyc and b/media/__pycache__/media.cpython-310.pyc differ
 
media/__pycache__/utils.cpython-310.pyc CHANGED
Binary files a/media/__pycache__/utils.cpython-310.pyc and b/media/__pycache__/utils.cpython-310.pyc differ
 
media/__pycache__/video_writer.cpython-310.pyc CHANGED
Binary files a/media/__pycache__/video_writer.cpython-310.pyc and b/media/__pycache__/video_writer.cpython-310.pyc differ
 
package.json CHANGED
@@ -1,6 +1,6 @@
1
  {
2
  "name": "trackio",
3
- "version": "0.7.0",
4
  "description": "",
5
  "python": "true"
6
  }
 
1
  {
2
  "name": "trackio",
3
+ "version": "0.10.0",
4
  "description": "",
5
  "python": "true"
6
  }
run.py CHANGED
@@ -12,6 +12,7 @@ from trackio.media import TrackioMedia
12
  from trackio.sqlite_storage import SQLiteStorage
13
  from trackio.table import Table
14
  from trackio.typehints import LogEntry, UploadEntry
 
15
 
16
  BATCH_SEND_INTERVAL = 0.5
17
 
@@ -62,16 +63,13 @@ class Run:
62
  def _get_username(self) -> str | None:
63
  """Get the current HuggingFace username if logged in, otherwise None."""
64
  try:
65
- who = huggingface_hub.whoami()
66
- return who["name"] if who else None
67
  except Exception:
68
  return None
69
 
70
  def _batch_sender(self):
71
  """Send batched logs every BATCH_SEND_INTERVAL."""
72
  while not self._stop_flag.is_set() or len(self._queued_logs) > 0:
73
- # If the stop flag has been set, then just quickly send all
74
- # the logs and exit.
75
  if not self._stop_flag.is_set():
76
  time.sleep(BATCH_SEND_INTERVAL)
77
 
@@ -112,36 +110,60 @@ class Run:
112
 
113
  self._batch_sender()
114
 
115
- def _process_media(self, metrics, step: int | None) -> dict:
 
 
 
 
 
 
 
 
 
 
 
116
  """
117
  Serialize media in metrics and upload to space if needed.
118
  """
119
- serializable_metrics = {}
120
- if not step:
121
- step = 0
122
- for key, value in metrics.items():
123
- if isinstance(value, TrackioMedia):
124
- value._save(self.project, self.name, step)
125
- serializable_metrics[key] = value._to_dict()
126
- if self._space_id:
127
- # Upload local media when deploying to space
128
- upload_entry: UploadEntry = {
129
- "project": self.project,
130
- "run": self.name,
131
- "step": step,
132
- "uploaded_file": handle_file(value._get_absolute_file_path()),
133
- }
134
- with self._client_lock:
135
- self._queued_uploads.append(upload_entry)
136
- else:
137
- serializable_metrics[key] = value
138
- return serializable_metrics
139
 
140
- @staticmethod
141
- def _replace_tables(metrics):
142
- for k, v in metrics.items():
143
- if isinstance(v, (Table, Histogram)):
144
- metrics[k] = v._to_dict()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
145
 
146
  def log(self, metrics: dict, step: int | None = None):
147
  renamed_keys = []
@@ -159,9 +181,16 @@ class Run:
159
  warnings.warn(f"Reserved keys renamed: {renamed_keys} → '__{{key}}'")
160
 
161
  metrics = new_metrics
162
- Run._replace_tables(metrics)
163
-
164
- metrics = self._process_media(metrics, step)
 
 
 
 
 
 
 
165
  metrics = utils.serialize_values(metrics)
166
 
167
  config_to_log = None
@@ -184,7 +213,6 @@ class Run:
184
  """Cleanup when run is finished."""
185
  self._stop_flag.set()
186
 
187
- # Wait for the batch sender to finish before joining the client thread.
188
  time.sleep(2 * BATCH_SEND_INTERVAL)
189
 
190
  if self._client_thread is not None:
 
12
  from trackio.sqlite_storage import SQLiteStorage
13
  from trackio.table import Table
14
  from trackio.typehints import LogEntry, UploadEntry
15
+ from trackio.utils import _get_default_namespace
16
 
17
  BATCH_SEND_INTERVAL = 0.5
18
 
 
63
  def _get_username(self) -> str | None:
64
  """Get the current HuggingFace username if logged in, otherwise None."""
65
  try:
66
+ return _get_default_namespace()
 
67
  except Exception:
68
  return None
69
 
70
  def _batch_sender(self):
71
  """Send batched logs every BATCH_SEND_INTERVAL."""
72
  while not self._stop_flag.is_set() or len(self._queued_logs) > 0:
 
 
73
  if not self._stop_flag.is_set():
74
  time.sleep(BATCH_SEND_INTERVAL)
75
 
 
110
 
111
  self._batch_sender()
112
 
113
+ def _queue_upload(self, file_path, step: int | None):
114
+ """Queue a media file for upload to space."""
115
+ upload_entry: UploadEntry = {
116
+ "project": self.project,
117
+ "run": self.name,
118
+ "step": step,
119
+ "uploaded_file": handle_file(file_path),
120
+ }
121
+ with self._client_lock:
122
+ self._queued_uploads.append(upload_entry)
123
+
124
+ def _process_media(self, value: TrackioMedia, step: int | None) -> dict:
125
  """
126
  Serialize media in metrics and upload to space if needed.
127
  """
128
+ value._save(self.project, self.name, step)
129
+ if self._space_id:
130
+ self._queue_upload(value._get_absolute_file_path(), step)
131
+ return value._to_dict()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
132
 
133
+ def _scan_and_queue_media_uploads(self, table_dict: dict, step: int | None):
134
+ """
135
+ Scan a serialized table for media objects and queue them for upload to space.
136
+ """
137
+ if not self._space_id:
138
+ return
139
+
140
+ table_data = table_dict.get("_value", [])
141
+ for row in table_data:
142
+ for value in row.values():
143
+ if isinstance(value, dict) and value.get("_type") in [
144
+ "trackio.image",
145
+ "trackio.video",
146
+ "trackio.audio",
147
+ ]:
148
+ file_path = value.get("file_path")
149
+ if file_path:
150
+ from trackio.utils import MEDIA_DIR
151
+
152
+ absolute_path = MEDIA_DIR / file_path
153
+ self._queue_upload(absolute_path, step)
154
+ elif isinstance(value, list):
155
+ for item in value:
156
+ if isinstance(item, dict) and item.get("_type") in [
157
+ "trackio.image",
158
+ "trackio.video",
159
+ "trackio.audio",
160
+ ]:
161
+ file_path = item.get("file_path")
162
+ if file_path:
163
+ from trackio.utils import MEDIA_DIR
164
+
165
+ absolute_path = MEDIA_DIR / file_path
166
+ self._queue_upload(absolute_path, step)
167
 
168
  def log(self, metrics: dict, step: int | None = None):
169
  renamed_keys = []
 
181
  warnings.warn(f"Reserved keys renamed: {renamed_keys} → '__{{key}}'")
182
 
183
  metrics = new_metrics
184
+ for key, value in metrics.items():
185
+ if isinstance(value, Table):
186
+ metrics[key] = value._to_dict(
187
+ project=self.project, run=self.name, step=step
188
+ )
189
+ self._scan_and_queue_media_uploads(metrics[key], step)
190
+ elif isinstance(value, Histogram):
191
+ metrics[key] = value._to_dict()
192
+ elif isinstance(value, TrackioMedia):
193
+ metrics[key] = self._process_media(value, step)
194
  metrics = utils.serialize_values(metrics)
195
 
196
  config_to_log = None
 
213
  """Cleanup when run is finished."""
214
  self._stop_flag.set()
215
 
 
216
  time.sleep(2 * BATCH_SEND_INTERVAL)
217
 
218
  if self._client_thread is not None:
table.py CHANGED
@@ -1,18 +1,27 @@
 
1
  from typing import Any, Literal
2
 
3
  from pandas import DataFrame
4
 
 
 
 
 
 
 
 
5
 
6
  class Table:
7
  """
8
- Initializes a Table object.
9
 
10
  Args:
11
  columns (`list[str]`, *optional*):
12
  Names of the columns in the table. Optional if `data` is provided. Not
13
  expected if `dataframe` is provided. Currently ignored.
14
  data (`list[list[Any]]`, *optional*):
15
- 2D row-oriented array of values.
 
16
  dataframe (`pandas.`DataFrame``, *optional*):
17
  DataFrame object used to create the table. When set, `data` and `columns`
18
  arguments are ignored.
@@ -40,14 +49,115 @@ class Table:
40
  ):
41
  # TODO: implement support for columns, dtype, optional, allow_mixed_types, and log_mode.
42
  # for now (like `rows`) they are included for API compat but don't do anything.
43
-
44
  if dataframe is None:
45
- self.data = data
46
  else:
47
- self.data = dataframe.to_dict(orient="records")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
48
 
49
- def _to_dict(self):
 
 
 
 
 
50
  return {
51
  "_type": self.TYPE,
52
- "_value": self.data,
53
  }
 
1
+ import os
2
  from typing import Any, Literal
3
 
4
  from pandas import DataFrame
5
 
6
+ try:
7
+ from trackio.media.media import TrackioMedia
8
+ from trackio.utils import MEDIA_DIR
9
+ except ImportError:
10
+ from media.media import TrackioMedia
11
+ from utils import MEDIA_DIR
12
+
13
 
14
  class Table:
15
  """
16
+ Initializes a Table object. Tables can be used to log tabular data including images, numbers, and text.
17
 
18
  Args:
19
  columns (`list[str]`, *optional*):
20
  Names of the columns in the table. Optional if `data` is provided. Not
21
  expected if `dataframe` is provided. Currently ignored.
22
  data (`list[list[Any]]`, *optional*):
23
+ 2D row-oriented array of values. Each value can be: a number, a string (treated as Markdown and truncated if too long),
24
+ or a `Trackio.Image` or list of `Trackio.Image` objects.
25
  dataframe (`pandas.`DataFrame``, *optional*):
26
  DataFrame object used to create the table. When set, `data` and `columns`
27
  arguments are ignored.
 
49
  ):
50
  # TODO: implement support for columns, dtype, optional, allow_mixed_types, and log_mode.
51
  # for now (like `rows`) they are included for API compat but don't do anything.
 
52
  if dataframe is None:
53
+ self.data = DataFrame(data) if data is not None else DataFrame()
54
  else:
55
+ self.data = dataframe
56
+
57
+ def _has_media_objects(self, dataframe: DataFrame) -> bool:
58
+ """Check if dataframe contains any TrackioMedia objects or lists of TrackioMedia objects."""
59
+ for col in dataframe.columns:
60
+ if dataframe[col].apply(lambda x: isinstance(x, TrackioMedia)).any():
61
+ return True
62
+ if (
63
+ dataframe[col]
64
+ .apply(
65
+ lambda x: isinstance(x, list)
66
+ and len(x) > 0
67
+ and isinstance(x[0], TrackioMedia)
68
+ )
69
+ .any()
70
+ ):
71
+ return True
72
+ return False
73
+
74
+ def _process_data(self, project: str, run: str, step: int = 0):
75
+ """Convert dataframe to dict format, processing any TrackioMedia objects if present."""
76
+ df = self.data
77
+ if not self._has_media_objects(df):
78
+ return df.to_dict(orient="records")
79
+
80
+ processed_df = df.copy()
81
+ for col in processed_df.columns:
82
+ for idx in processed_df.index:
83
+ value = processed_df.at[idx, col]
84
+ if isinstance(value, TrackioMedia):
85
+ value._save(project, run, step)
86
+ processed_df.at[idx, col] = value._to_dict()
87
+ if (
88
+ isinstance(value, list)
89
+ and len(value) > 0
90
+ and isinstance(value[0], TrackioMedia)
91
+ ):
92
+ [v._save(project, run, step) for v in value]
93
+ processed_df.at[idx, col] = [v._to_dict() for v in value]
94
+
95
+ return processed_df.to_dict(orient="records")
96
+
97
+ @staticmethod
98
+ def to_display_format(table_data: list[dict]) -> list[dict]:
99
+ """Convert stored table data to display format for UI rendering. Note
100
+ that this does not use the self.data attribute, but instead uses the
101
+ table_data parameter, which is is what the UI receives.
102
+
103
+ Args:
104
+ table_data: List of dictionaries representing table rows (from stored _value)
105
+
106
+ Returns:
107
+ Table data with images converted to markdown syntax and long text truncated.
108
+ """
109
+ truncate_length = int(os.getenv("TRACKIO_TABLE_TRUNCATE_LENGTH", "250"))
110
+
111
+ def convert_image_to_markdown(image_data: dict) -> str:
112
+ relative_path = image_data.get("file_path", "")
113
+ caption = image_data.get("caption", "")
114
+ absolute_path = MEDIA_DIR / relative_path
115
+ return f'<img src="/gradio_api/file={absolute_path}" alt="{caption}" />'
116
+
117
+ processed_data = []
118
+ for row in table_data:
119
+ processed_row = {}
120
+ for key, value in row.items():
121
+ if isinstance(value, dict) and value.get("_type") == "trackio.image":
122
+ processed_row[key] = convert_image_to_markdown(value)
123
+ elif (
124
+ isinstance(value, list)
125
+ and len(value) > 0
126
+ and isinstance(value[0], dict)
127
+ and value[0].get("_type") == "trackio.image"
128
+ ):
129
+ # This assumes that if the first item is an image, all items are images. Ok for now since we don't support mixed types in a single cell.
130
+ processed_row[key] = (
131
+ '<div style="display: flex; gap: 10px;">'
132
+ + "".join([convert_image_to_markdown(item) for item in value])
133
+ + "</div>"
134
+ )
135
+ elif isinstance(value, str) and len(value) > truncate_length:
136
+ truncated = value[:truncate_length]
137
+ full_text = value.replace("<", "&lt;").replace(">", "&gt;")
138
+ processed_row[key] = (
139
+ f'<details style="display: inline;">'
140
+ f'<summary style="display: inline; cursor: pointer;">{truncated}…<span><em>(truncated, click to expand)</em></span></summary>'
141
+ f'<div style="margin-top: 10px; padding: 10px; background: #f5f5f5; border-radius: 4px; max-height: 400px; overflow: auto;">'
142
+ f'<pre style="white-space: pre-wrap; word-wrap: break-word; margin: 0;">{full_text}</pre>'
143
+ f"</div>"
144
+ f"</details>"
145
+ )
146
+ else:
147
+ processed_row[key] = value
148
+ processed_data.append(processed_row)
149
+ return processed_data
150
+
151
+ def _to_dict(self, project: str, run: str, step: int = 0):
152
+ """Convert table to dictionary representation.
153
 
154
+ Args:
155
+ project: Project name for saving media files
156
+ run: Run name for saving media files
157
+ step: Step number for saving media files
158
+ """
159
+ data = self._process_data(project, run, step)
160
  return {
161
  "_type": self.TYPE,
162
+ "_value": data,
163
  }
ui/__pycache__/__init__.cpython-310.pyc CHANGED
Binary files a/ui/__pycache__/__init__.cpython-310.pyc and b/ui/__pycache__/__init__.cpython-310.pyc differ
 
ui/__pycache__/fns.cpython-310.pyc CHANGED
Binary files a/ui/__pycache__/fns.cpython-310.pyc and b/ui/__pycache__/fns.cpython-310.pyc differ
 
ui/__pycache__/main.cpython-310.pyc CHANGED
Binary files a/ui/__pycache__/main.cpython-310.pyc and b/ui/__pycache__/main.cpython-310.pyc differ
 
ui/__pycache__/run_detail.cpython-310.pyc CHANGED
Binary files a/ui/__pycache__/run_detail.cpython-310.pyc and b/ui/__pycache__/run_detail.cpython-310.pyc differ
 
ui/__pycache__/runs.cpython-310.pyc CHANGED
Binary files a/ui/__pycache__/runs.cpython-310.pyc and b/ui/__pycache__/runs.cpython-310.pyc differ
 
ui/fns.py CHANGED
@@ -1,6 +1,7 @@
1
  """Shared functions for the Trackio UI."""
2
 
3
  import os
 
4
 
5
  import gradio as gr
6
  import huggingface_hub as hf
@@ -82,6 +83,7 @@ def update_navbar_value(project_dd, request: gr.Request):
82
  )
83
 
84
 
 
85
  def check_hf_token_has_write_access(hf_token: str | None) -> None:
86
  """
87
  Checks to see if the provided hf_token is valid and has write access to the Space
@@ -137,6 +139,7 @@ def check_hf_token_has_write_access(hf_token: str | None) -> None:
137
  )
138
 
139
 
 
140
  def check_oauth_token_has_write_access(oauth_token: str | None) -> None:
141
  """
142
  Checks to see if the oauth token provided via Gradio's OAuth is valid and has write access
 
1
  """Shared functions for the Trackio UI."""
2
 
3
  import os
4
+ from functools import lru_cache
5
 
6
  import gradio as gr
7
  import huggingface_hub as hf
 
83
  )
84
 
85
 
86
+ @lru_cache(maxsize=32)
87
  def check_hf_token_has_write_access(hf_token: str | None) -> None:
88
  """
89
  Checks to see if the provided hf_token is valid and has write access to the Space
 
139
  )
140
 
141
 
142
+ @lru_cache(maxsize=32)
143
  def check_oauth_token_has_write_access(oauth_token: str | None) -> None:
144
  """
145
  Checks to see if the oauth token provided via Gradio's OAuth is valid and has write access
ui/helpers/__pycache__/run_selection.cpython-310.pyc CHANGED
Binary files a/ui/helpers/__pycache__/run_selection.cpython-310.pyc and b/ui/helpers/__pycache__/run_selection.cpython-310.pyc differ
 
ui/main.py CHANGED
@@ -99,6 +99,18 @@ def get_runs(project) -> list[str]:
99
  return SQLiteStorage.get_runs(project)
100
 
101
 
 
 
 
 
 
 
 
 
 
 
 
 
102
  def get_available_metrics(project: str, runs: list[str]) -> list[str]:
103
  """Get all available metrics across all runs for x-axis selection."""
104
  if not project or not runs:
@@ -162,9 +174,10 @@ def extract_media(logs: list[dict]) -> dict[str, list[MediaData]]:
162
  def load_run_data(
163
  project: str | None,
164
  run: str | None,
165
- smoothing_granularity: int,
166
- x_axis: str,
167
- log_scale: bool = False,
 
168
  ) -> tuple[pd.DataFrame, dict]:
169
  if not project or not run:
170
  return None, None
@@ -189,13 +202,26 @@ def load_run_data(
189
  else:
190
  x_column = x_axis
191
 
192
- if log_scale and x_column in df.columns:
193
  x_vals = df[x_column]
194
  if (x_vals <= 0).any():
195
  df[x_column] = np.log10(np.maximum(x_vals, 0) + 1)
196
  else:
197
  df[x_column] = np.log10(x_vals)
198
 
 
 
 
 
 
 
 
 
 
 
 
 
 
199
  if smoothing_granularity > 0:
200
  numeric_cols = df.select_dtypes(include="number").columns
201
  numeric_cols = [c for c in numeric_cols if c not in utils.RESERVED_KEYS]
@@ -271,22 +297,6 @@ def toggle_timer(cb_value):
271
  return gr.Timer(active=False)
272
 
273
 
274
- def upload_db_to_space(
275
- project: str, uploaded_db: gr.FileData, hf_token: str | None
276
- ) -> None:
277
- """
278
- Uploads the database of a local Trackio project to a Hugging Face Space.
279
- """
280
- fns.check_hf_token_has_write_access(hf_token)
281
- db_project_path = SQLiteStorage.get_project_db_path(project)
282
- if os.path.exists(db_project_path):
283
- raise gr.Error(
284
- f"Trackio database file already exists for project {project}, cannot overwrite."
285
- )
286
- os.makedirs(os.path.dirname(db_project_path), exist_ok=True)
287
- shutil.copy(uploaded_db["path"], db_project_path)
288
-
289
-
290
  def bulk_upload_media(uploads: list[UploadEntry], hf_token: str | None) -> None:
291
  """
292
  Uploads media files to a Trackio dashboard. Each entry in the list is a tuple of the project, run, and media file to be uploaded.
@@ -548,7 +558,7 @@ def create_media_section(media_by_run: dict[str, dict[str, list[MediaData]]]):
548
  )
549
 
550
 
551
- css = """
552
  #run-cb .wrap { gap: 2px; }
553
  #run-cb .wrap label {
554
  line-height: 1;
@@ -603,9 +613,13 @@ css = """
603
  gap: 0.25em;
604
  margin-bottom: 0.25em;
605
  }
 
 
 
 
606
  """
607
 
608
- javascript = """
609
  <script>
610
  function setCookie(name, value, days) {
611
  var expires = "";
@@ -631,6 +645,7 @@ function getCookie(name) {
631
  (function() {
632
  const urlParams = new URLSearchParams(window.location.search);
633
  const writeToken = urlParams.get('write_token');
 
634
 
635
  if (writeToken) {
636
  setCookie('trackio_write_token', writeToken, 7);
@@ -646,6 +661,12 @@ function getCookie(name) {
646
  window.history.replaceState({}, document.title, newUrl);
647
  }
648
  }
 
 
 
 
 
 
649
  })();
650
  </script>
651
  """
@@ -653,7 +674,7 @@ function getCookie(name) {
653
 
654
  gr.set_static_paths(paths=[utils.MEDIA_DIR])
655
 
656
- with gr.Blocks(title="Trackio Dashboard", css=css, head=javascript) as demo:
657
  with gr.Sidebar(open=False) as sidebar:
658
  logo_urls = utils.get_logo_urls()
659
  logo = gr.Markdown(
@@ -698,7 +719,8 @@ with gr.Blocks(title="Trackio Dashboard", css=css, head=javascript) as demo:
698
  choices=["step", "time"],
699
  value="step",
700
  )
701
- log_scale_cb = gr.Checkbox(label="Log scale X-axis", value=False)
 
702
  metric_filter_tb = gr.Textbox(
703
  label="Metric Filter (regex)",
704
  placeholder="e.g., loss|ndcg@10|gpu",
@@ -726,7 +748,7 @@ with gr.Blocks(title="Trackio Dashboard", css=css, head=javascript) as demo:
726
  smoothing_slider,
727
  ],
728
  queue=False,
729
- api_name=False,
730
  )
731
  gr.on(
732
  [demo.load],
@@ -734,7 +756,7 @@ with gr.Blocks(title="Trackio Dashboard", css=css, head=javascript) as demo:
734
  outputs=project_dd,
735
  show_progress="hidden",
736
  queue=False,
737
- api_name=False,
738
  )
739
  gr.on(
740
  [timer.tick],
@@ -742,14 +764,14 @@ with gr.Blocks(title="Trackio Dashboard", css=css, head=javascript) as demo:
742
  inputs=[project_dd, run_tb, run_selection_state, selected_runs_from_url],
743
  outputs=[run_cb, run_tb, run_selection_state],
744
  show_progress="hidden",
745
- api_name=False,
746
  )
747
  gr.on(
748
  [timer.tick],
749
  fn=lambda: gr.Dropdown(info=fns.get_project_info()),
750
  outputs=[project_dd],
751
  show_progress="hidden",
752
- api_name=False,
753
  )
754
  gr.on(
755
  [demo.load, project_dd.change],
@@ -758,34 +780,34 @@ with gr.Blocks(title="Trackio Dashboard", css=css, head=javascript) as demo:
758
  outputs=[run_cb, run_tb, run_selection_state],
759
  show_progress="hidden",
760
  queue=False,
761
- api_name=False,
762
  ).then(
763
  fn=update_x_axis_choices,
764
  inputs=[project_dd, run_selection_state],
765
  outputs=x_axis_dd,
766
  show_progress="hidden",
767
  queue=False,
768
- api_name=False,
769
  ).then(
770
  fn=generate_embed,
771
  inputs=[project_dd, metric_filter_tb, run_selection_state],
772
  outputs=[embed_code],
773
  show_progress="hidden",
774
- api_name=False,
775
  queue=False,
776
  ).then(
777
  fns.update_navbar_value,
778
  inputs=[project_dd],
779
  outputs=[navbar],
780
  show_progress="hidden",
781
- api_name=False,
782
  queue=False,
783
  ).then(
784
  fn=fns.get_group_by_fields,
785
  inputs=[project_dd],
786
  outputs=[run_group_by_dd],
787
  show_progress="hidden",
788
- api_name=False,
789
  queue=False,
790
  )
791
 
@@ -796,7 +818,7 @@ with gr.Blocks(title="Trackio Dashboard", css=css, head=javascript) as demo:
796
  outputs=x_axis_dd,
797
  show_progress="hidden",
798
  queue=False,
799
- api_name=False,
800
  )
801
  gr.on(
802
  [metric_filter_tb.change, run_cb.change],
@@ -804,7 +826,7 @@ with gr.Blocks(title="Trackio Dashboard", css=css, head=javascript) as demo:
804
  inputs=[project_dd, metric_filter_tb, run_selection_state],
805
  outputs=embed_code,
806
  show_progress="hidden",
807
- api_name=False,
808
  queue=False,
809
  )
810
 
@@ -820,7 +842,7 @@ with gr.Blocks(title="Trackio Dashboard", css=css, head=javascript) as demo:
820
  inputs=[run_group_by_dd],
821
  outputs=[run_cb, grouped_runs_panel],
822
  show_progress="hidden",
823
- api_name=False,
824
  queue=False,
825
  )
826
 
@@ -828,28 +850,28 @@ with gr.Blocks(title="Trackio Dashboard", css=css, head=javascript) as demo:
828
  fn=toggle_timer,
829
  inputs=realtime_cb,
830
  outputs=timer,
831
- api_name=False,
832
  queue=False,
833
  )
834
  run_cb.input(
835
  fn=fns.handle_run_checkbox_change,
836
  inputs=[run_cb, run_selection_state],
837
  outputs=run_selection_state,
838
- api_name=False,
839
  queue=False,
840
  ).then(
841
  fn=generate_embed,
842
  inputs=[project_dd, metric_filter_tb, run_selection_state],
843
  outputs=embed_code,
844
  show_progress="hidden",
845
- api_name=False,
846
  queue=False,
847
  )
848
  run_tb.input(
849
  fn=refresh_runs,
850
  inputs=[project_dd, run_tb, run_selection_state],
851
  outputs=[run_cb, run_tb, run_selection_state],
852
- api_name=False,
853
  queue=False,
854
  show_progress="hidden",
855
  )
@@ -911,7 +933,7 @@ with gr.Blocks(title="Trackio Dashboard", css=css, head=javascript) as demo:
911
  inputs=[project_dd],
912
  outputs=last_steps,
913
  show_progress="hidden",
914
- api_name=False,
915
  )
916
 
917
  @gr.render(
@@ -922,7 +944,8 @@ with gr.Blocks(title="Trackio Dashboard", css=css, head=javascript) as demo:
922
  smoothing_slider.change,
923
  x_lim.change,
924
  x_axis_dd.change,
925
- log_scale_cb.change,
 
926
  metric_filter_tb.change,
927
  ],
928
  inputs=[
@@ -932,7 +955,8 @@ with gr.Blocks(title="Trackio Dashboard", css=css, head=javascript) as demo:
932
  metrics_subset,
933
  x_lim,
934
  x_axis_dd,
935
- log_scale_cb,
 
936
  metric_filter_tb,
937
  ],
938
  show_progress="hidden",
@@ -945,7 +969,8 @@ with gr.Blocks(title="Trackio Dashboard", css=css, head=javascript) as demo:
945
  metrics_subset,
946
  x_lim_value,
947
  x_axis,
948
- log_scale,
 
949
  metric_filter,
950
  ):
951
  dfs = []
@@ -954,7 +979,7 @@ with gr.Blocks(title="Trackio Dashboard", css=css, head=javascript) as demo:
954
 
955
  for run in runs:
956
  df, media_by_key = load_run_data(
957
- project, run, smoothing_granularity, x_axis, log_scale
958
  )
959
  if df is not None:
960
  dfs.append(df)
@@ -1058,13 +1083,13 @@ with gr.Blocks(title="Trackio Dashboard", css=css, head=javascript) as demo:
1058
  y_title=metric_name.split("/")[-1],
1059
  color=color,
1060
  color_map=color_map,
 
1061
  title=metric_name,
1062
  key=f"plot-{metric_idx}",
1063
  preserved_by_key=None,
 
1064
  x_lim=updated_x_lim,
1065
- show_fullscreen_button=True,
1066
  min_width=400,
1067
- show_export_button=True,
1068
  )
1069
  plot.select(
1070
  update_x_lim,
@@ -1123,13 +1148,13 @@ with gr.Blocks(title="Trackio Dashboard", css=css, head=javascript) as demo:
1123
  y_title=metric_name.split("/")[-1],
1124
  color=color,
1125
  color_map=color_map,
 
1126
  title=metric_name,
1127
  key=f"plot-{metric_idx}",
1128
  preserved_by_key=None,
 
1129
  x_lim=updated_x_lim,
1130
- show_fullscreen_button=True,
1131
  min_width=400,
1132
- show_export_button=True,
1133
  )
1134
  plot.select(
1135
  update_x_lim,
@@ -1145,59 +1170,113 @@ with gr.Blocks(title="Trackio Dashboard", css=css, head=javascript) as demo:
1145
  if media_by_run and any(any(media) for media in media_by_run.values()):
1146
  create_media_section(media_by_run)
1147
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1148
  table_cols = master_df.select_dtypes(include="object").columns
1149
  table_cols = [c for c in table_cols if c not in utils.RESERVED_KEYS]
1150
- if metrics_subset:
1151
- table_cols = [c for c in table_cols if c in metrics_subset]
1152
  if metric_filter and metric_filter.strip():
1153
  table_cols = filter_metrics_by_regex(list(table_cols), metric_filter)
 
 
 
 
 
 
 
1154
 
1155
- actual_table_count = sum(
1156
- 1
1157
- for metric_name in table_cols
1158
- if not (metric_df := master_df.dropna(subset=[metric_name])).empty
1159
- and isinstance(value := metric_df[metric_name].iloc[-1], dict)
1160
- and value.get("_type") == Table.TYPE
1161
- )
1162
-
1163
- if actual_table_count > 0:
1164
- with gr.Accordion(f"tables ({actual_table_count})", open=True):
1165
  with gr.Row(key="row"):
1166
  for metric_idx, metric_name in enumerate(table_cols):
1167
  metric_df = master_df.dropna(subset=[metric_name])
1168
  if not metric_df.empty:
1169
- value = metric_df[metric_name].iloc[-1]
 
1170
  if (
1171
- isinstance(value, dict)
1172
- and "_type" in value
1173
- and value["_type"] == Table.TYPE
1174
  ):
1175
  try:
1176
- df = pd.DataFrame(value["_value"])
1177
- gr.DataFrame(
1178
- df,
1179
- label=f"{metric_name} (latest)",
1180
- key=f"table-{metric_idx}",
1181
- wrap=True,
1182
- )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1183
  except Exception as e:
1184
  gr.Warning(
1185
  f"Column {metric_name} failed to render as a table: {e}"
1186
  )
1187
 
1188
- # Display histograms
1189
  histogram_cols = set(master_df.columns) - {
1190
  "run",
1191
  "step",
1192
  "timestamp",
1193
  "data_type",
1194
  }
1195
- if metrics_subset:
1196
- histogram_cols = [c for c in histogram_cols if c in metrics_subset]
1197
- if metric_filter and metric_filter.strip():
1198
- histogram_cols = filter_metrics_by_regex(
1199
- list(histogram_cols), metric_filter
1200
- )
1201
 
1202
  actual_histogram_count = sum(
1203
  1
@@ -1338,7 +1417,7 @@ with gr.Blocks(title="Trackio Dashboard", css=css, head=javascript) as demo:
1338
  run_cb,
1339
  ],
1340
  show_progress="hidden",
1341
- api_name=False,
1342
  queue=False,
1343
  )
1344
 
@@ -1352,7 +1431,7 @@ with gr.Blocks(title="Trackio Dashboard", css=css, head=javascript) as demo:
1352
  ],
1353
  outputs=[run_selection_state, group_cb, run_cb],
1354
  show_progress="hidden",
1355
- api_name=False,
1356
  queue=False,
1357
  )
1358
 
 
99
  return SQLiteStorage.get_runs(project)
100
 
101
 
102
+ def upload_db_to_space(
103
+ project: str, uploaded_db: gr.FileData, hf_token: str | None
104
+ ) -> None:
105
+ """
106
+ Uploads the database of a local Trackio project to a Hugging Face Space.
107
+ """
108
+ fns.check_hf_token_has_write_access(hf_token)
109
+ db_project_path = SQLiteStorage.get_project_db_path(project)
110
+ os.makedirs(os.path.dirname(db_project_path), exist_ok=True)
111
+ shutil.copy(uploaded_db["path"], db_project_path)
112
+
113
+
114
  def get_available_metrics(project: str, runs: list[str]) -> list[str]:
115
  """Get all available metrics across all runs for x-axis selection."""
116
  if not project or not runs:
 
174
  def load_run_data(
175
  project: str | None,
176
  run: str | None,
177
+ smoothing_granularity: int = 0,
178
+ x_axis: str = "step",
179
+ log_scale_x: bool = False,
180
+ log_scale_y: bool = False,
181
  ) -> tuple[pd.DataFrame, dict]:
182
  if not project or not run:
183
  return None, None
 
202
  else:
203
  x_column = x_axis
204
 
205
+ if log_scale_x and x_column in df.columns:
206
  x_vals = df[x_column]
207
  if (x_vals <= 0).any():
208
  df[x_column] = np.log10(np.maximum(x_vals, 0) + 1)
209
  else:
210
  df[x_column] = np.log10(x_vals)
211
 
212
+ if log_scale_y:
213
+ numeric_cols = df.select_dtypes(include="number").columns
214
+ y_cols = [
215
+ c for c in numeric_cols if c not in utils.RESERVED_KEYS and c != x_column
216
+ ]
217
+ for y_col in y_cols:
218
+ if y_col in df.columns:
219
+ y_vals = df[y_col]
220
+ if (y_vals <= 0).any():
221
+ df[y_col] = np.log10(np.maximum(y_vals, 0) + 1)
222
+ else:
223
+ df[y_col] = np.log10(y_vals)
224
+
225
  if smoothing_granularity > 0:
226
  numeric_cols = df.select_dtypes(include="number").columns
227
  numeric_cols = [c for c in numeric_cols if c not in utils.RESERVED_KEYS]
 
297
  return gr.Timer(active=False)
298
 
299
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
300
  def bulk_upload_media(uploads: list[UploadEntry], hf_token: str | None) -> None:
301
  """
302
  Uploads media files to a Trackio dashboard. Each entry in the list is a tuple of the project, run, and media file to be uploaded.
 
558
  )
559
 
560
 
561
+ CSS = """
562
  #run-cb .wrap { gap: 2px; }
563
  #run-cb .wrap label {
564
  line-height: 1;
 
613
  gap: 0.25em;
614
  margin-bottom: 0.25em;
615
  }
616
+
617
+ .tab-like-container {
618
+ visibility: hidden;
619
+ }
620
  """
621
 
622
+ HEAD = """
623
  <script>
624
  function setCookie(name, value, days) {
625
  var expires = "";
 
645
  (function() {
646
  const urlParams = new URLSearchParams(window.location.search);
647
  const writeToken = urlParams.get('write_token');
648
+ const footerParam = urlParams.get('footer');
649
 
650
  if (writeToken) {
651
  setCookie('trackio_write_token', writeToken, 7);
 
661
  window.history.replaceState({}, document.title, newUrl);
662
  }
663
  }
664
+
665
+ if (footerParam === 'false') {
666
+ const style = document.createElement('style');
667
+ style.textContent = 'footer { display: none !important; }';
668
+ document.head.appendChild(style);
669
+ }
670
  })();
671
  </script>
672
  """
 
674
 
675
  gr.set_static_paths(paths=[utils.MEDIA_DIR])
676
 
677
+ with gr.Blocks(title="Trackio Dashboard") as demo:
678
  with gr.Sidebar(open=False) as sidebar:
679
  logo_urls = utils.get_logo_urls()
680
  logo = gr.Markdown(
 
719
  choices=["step", "time"],
720
  value="step",
721
  )
722
+ log_scale_x_cb = gr.Checkbox(label="Log scale X-axis", value=False)
723
+ log_scale_y_cb = gr.Checkbox(label="Log scale Y-axis", value=False)
724
  metric_filter_tb = gr.Textbox(
725
  label="Metric Filter (regex)",
726
  placeholder="e.g., loss|ndcg@10|gpu",
 
748
  smoothing_slider,
749
  ],
750
  queue=False,
751
+ api_visibility="private",
752
  )
753
  gr.on(
754
  [demo.load],
 
756
  outputs=project_dd,
757
  show_progress="hidden",
758
  queue=False,
759
+ api_visibility="private",
760
  )
761
  gr.on(
762
  [timer.tick],
 
764
  inputs=[project_dd, run_tb, run_selection_state, selected_runs_from_url],
765
  outputs=[run_cb, run_tb, run_selection_state],
766
  show_progress="hidden",
767
+ api_visibility="private",
768
  )
769
  gr.on(
770
  [timer.tick],
771
  fn=lambda: gr.Dropdown(info=fns.get_project_info()),
772
  outputs=[project_dd],
773
  show_progress="hidden",
774
+ api_visibility="private",
775
  )
776
  gr.on(
777
  [demo.load, project_dd.change],
 
780
  outputs=[run_cb, run_tb, run_selection_state],
781
  show_progress="hidden",
782
  queue=False,
783
+ api_visibility="private",
784
  ).then(
785
  fn=update_x_axis_choices,
786
  inputs=[project_dd, run_selection_state],
787
  outputs=x_axis_dd,
788
  show_progress="hidden",
789
  queue=False,
790
+ api_visibility="private",
791
  ).then(
792
  fn=generate_embed,
793
  inputs=[project_dd, metric_filter_tb, run_selection_state],
794
  outputs=[embed_code],
795
  show_progress="hidden",
796
+ api_visibility="private",
797
  queue=False,
798
  ).then(
799
  fns.update_navbar_value,
800
  inputs=[project_dd],
801
  outputs=[navbar],
802
  show_progress="hidden",
803
+ api_visibility="private",
804
  queue=False,
805
  ).then(
806
  fn=fns.get_group_by_fields,
807
  inputs=[project_dd],
808
  outputs=[run_group_by_dd],
809
  show_progress="hidden",
810
+ api_visibility="private",
811
  queue=False,
812
  )
813
 
 
818
  outputs=x_axis_dd,
819
  show_progress="hidden",
820
  queue=False,
821
+ api_visibility="private",
822
  )
823
  gr.on(
824
  [metric_filter_tb.change, run_cb.change],
 
826
  inputs=[project_dd, metric_filter_tb, run_selection_state],
827
  outputs=embed_code,
828
  show_progress="hidden",
829
+ api_visibility="private",
830
  queue=False,
831
  )
832
 
 
842
  inputs=[run_group_by_dd],
843
  outputs=[run_cb, grouped_runs_panel],
844
  show_progress="hidden",
845
+ api_visibility="private",
846
  queue=False,
847
  )
848
 
 
850
  fn=toggle_timer,
851
  inputs=realtime_cb,
852
  outputs=timer,
853
+ api_visibility="private",
854
  queue=False,
855
  )
856
  run_cb.input(
857
  fn=fns.handle_run_checkbox_change,
858
  inputs=[run_cb, run_selection_state],
859
  outputs=run_selection_state,
860
+ api_visibility="private",
861
  queue=False,
862
  ).then(
863
  fn=generate_embed,
864
  inputs=[project_dd, metric_filter_tb, run_selection_state],
865
  outputs=embed_code,
866
  show_progress="hidden",
867
+ api_visibility="private",
868
  queue=False,
869
  )
870
  run_tb.input(
871
  fn=refresh_runs,
872
  inputs=[project_dd, run_tb, run_selection_state],
873
  outputs=[run_cb, run_tb, run_selection_state],
874
+ api_visibility="private",
875
  queue=False,
876
  show_progress="hidden",
877
  )
 
933
  inputs=[project_dd],
934
  outputs=last_steps,
935
  show_progress="hidden",
936
+ api_visibility="private",
937
  )
938
 
939
  @gr.render(
 
944
  smoothing_slider.change,
945
  x_lim.change,
946
  x_axis_dd.change,
947
+ log_scale_x_cb.change,
948
+ log_scale_y_cb.change,
949
  metric_filter_tb.change,
950
  ],
951
  inputs=[
 
955
  metrics_subset,
956
  x_lim,
957
  x_axis_dd,
958
+ log_scale_x_cb,
959
+ log_scale_y_cb,
960
  metric_filter_tb,
961
  ],
962
  show_progress="hidden",
 
969
  metrics_subset,
970
  x_lim_value,
971
  x_axis,
972
+ log_scale_x,
973
+ log_scale_y,
974
  metric_filter,
975
  ):
976
  dfs = []
 
979
 
980
  for run in runs:
981
  df, media_by_key = load_run_data(
982
+ project, run, smoothing_granularity, x_axis, log_scale_x, log_scale_y
983
  )
984
  if df is not None:
985
  dfs.append(df)
 
1083
  y_title=metric_name.split("/")[-1],
1084
  color=color,
1085
  color_map=color_map,
1086
+ colors_in_legend=original_runs,
1087
  title=metric_name,
1088
  key=f"plot-{metric_idx}",
1089
  preserved_by_key=None,
1090
+ buttons=["fullscreen", "export"],
1091
  x_lim=updated_x_lim,
 
1092
  min_width=400,
 
1093
  )
1094
  plot.select(
1095
  update_x_lim,
 
1148
  y_title=metric_name.split("/")[-1],
1149
  color=color,
1150
  color_map=color_map,
1151
+ colors_in_legend=original_runs,
1152
  title=metric_name,
1153
  key=f"plot-{metric_idx}",
1154
  preserved_by_key=None,
1155
+ buttons=["fullscreen", "export"],
1156
  x_lim=updated_x_lim,
 
1157
  min_width=400,
 
1158
  )
1159
  plot.select(
1160
  update_x_lim,
 
1170
  if media_by_run and any(any(media) for media in media_by_run.values()):
1171
  create_media_section(media_by_run)
1172
 
1173
+ @gr.render(
1174
+ triggers=[
1175
+ demo.load,
1176
+ run_cb.change,
1177
+ last_steps.change,
1178
+ metric_filter_tb.change,
1179
+ ],
1180
+ inputs=[
1181
+ project_dd,
1182
+ run_cb,
1183
+ metrics_subset,
1184
+ metric_filter_tb,
1185
+ ],
1186
+ show_progress="hidden",
1187
+ queue=False,
1188
+ )
1189
+ def update_tables(
1190
+ project,
1191
+ runs,
1192
+ metrics_subset_value,
1193
+ metric_filter,
1194
+ ):
1195
+ dfs = []
1196
+ for run in runs:
1197
+ df, _ = load_run_data(project, run)
1198
+ if df is not None:
1199
+ dfs.append(df)
1200
+ master_df = pd.concat(dfs, ignore_index=True) if dfs else pd.DataFrame()
1201
+
1202
  table_cols = master_df.select_dtypes(include="object").columns
1203
  table_cols = [c for c in table_cols if c not in utils.RESERVED_KEYS]
1204
+ if metrics_subset_value:
1205
+ table_cols = [c for c in table_cols if c in metrics_subset_value]
1206
  if metric_filter and metric_filter.strip():
1207
  table_cols = filter_metrics_by_regex(list(table_cols), metric_filter)
1208
+ table_cols = [
1209
+ c
1210
+ for c in table_cols
1211
+ if not (metric_df := master_df.dropna(subset=[c])).empty
1212
+ and isinstance(first_value := metric_df[c].iloc[0], dict)
1213
+ and first_value.get("_type") == Table.TYPE
1214
+ ]
1215
 
1216
+ if len(table_cols) > 0:
1217
+ with gr.Accordion(f"tables ({len(table_cols)})", open=True):
 
 
 
 
 
 
 
 
1218
  with gr.Row(key="row"):
1219
  for metric_idx, metric_name in enumerate(table_cols):
1220
  metric_df = master_df.dropna(subset=[metric_name])
1221
  if not metric_df.empty:
1222
+ value = metric_df[metric_name]
1223
+ first_value = value.iloc[0]
1224
  if (
1225
+ isinstance(first_value, dict)
1226
+ and "_type" in first_value
1227
+ and first_value["_type"] == Table.TYPE
1228
  ):
1229
  try:
1230
+ with gr.Column():
1231
+ s = gr.Slider(
1232
+ value=len(value),
1233
+ minimum=1,
1234
+ maximum=len(value),
1235
+ step=1,
1236
+ container=False,
1237
+ visible=len(value) > 1,
1238
+ )
1239
+ processed_data = Table.to_display_format(
1240
+ value.iloc[-1]["_value"]
1241
+ )
1242
+ df = pd.DataFrame(processed_data)
1243
+ table = gr.DataFrame(
1244
+ df,
1245
+ label=f"{metric_name} (index {len(value)})",
1246
+ key=f"table-{metric_idx}",
1247
+ wrap=True,
1248
+ datatype="markdown",
1249
+ preserved_by_key=None,
1250
+ )
1251
+
1252
+ def get_table_at_index(index: int):
1253
+ value = metric_df[metric_name]
1254
+ processed_data = Table.to_display_format(
1255
+ value.iloc[index - 1]["_value"]
1256
+ )
1257
+ df_ = pd.DataFrame(processed_data)
1258
+ return gr.Dataframe(
1259
+ df_,
1260
+ label=f"{metric_name} (index {index})",
1261
+ )
1262
+
1263
+ s.input(
1264
+ get_table_at_index,
1265
+ inputs=s,
1266
+ outputs=table,
1267
+ show_progress="hidden",
1268
+ )
1269
  except Exception as e:
1270
  gr.Warning(
1271
  f"Column {metric_name} failed to render as a table: {e}"
1272
  )
1273
 
 
1274
  histogram_cols = set(master_df.columns) - {
1275
  "run",
1276
  "step",
1277
  "timestamp",
1278
  "data_type",
1279
  }
 
 
 
 
 
 
1280
 
1281
  actual_histogram_count = sum(
1282
  1
 
1417
  run_cb,
1418
  ],
1419
  show_progress="hidden",
1420
+ api_visibility="private",
1421
  queue=False,
1422
  )
1423
 
 
1431
  ],
1432
  outputs=[run_selection_state, group_cb, run_cb],
1433
  show_progress="hidden",
1434
+ api_visibility="private",
1435
  queue=False,
1436
  )
1437
 
ui/run_detail.py CHANGED
@@ -73,13 +73,13 @@ with gr.Blocks() as run_detail_page:
73
  outputs=[project_dd, run_dd],
74
  show_progress="hidden",
75
  queue=False,
76
- api_name=False,
77
  ).then(
78
  fns.update_navbar_value,
79
  inputs=[project_dd],
80
  outputs=[navbar],
81
  show_progress="hidden",
82
- api_name=False,
83
  queue=False,
84
  )
85
 
@@ -89,6 +89,6 @@ with gr.Blocks() as run_detail_page:
89
  inputs=[project_dd, run_dd],
90
  outputs=[run_details, run_config],
91
  show_progress="hidden",
92
- api_name=False,
93
  queue=False,
94
  )
 
73
  outputs=[project_dd, run_dd],
74
  show_progress="hidden",
75
  queue=False,
76
+ api_visibility="private",
77
  ).then(
78
  fns.update_navbar_value,
79
  inputs=[project_dd],
80
  outputs=[navbar],
81
  show_progress="hidden",
82
+ api_visibility="private",
83
  queue=False,
84
  )
85
 
 
89
  inputs=[project_dd, run_dd],
90
  outputs=[run_details, run_config],
91
  show_progress="hidden",
92
+ api_visibility="private",
93
  queue=False,
94
  )
ui/runs.py CHANGED
@@ -200,14 +200,14 @@ with gr.Blocks() as run_page:
200
  outputs=project_dd,
201
  show_progress="hidden",
202
  queue=False,
203
- api_name=False,
204
  )
205
  gr.on(
206
  [timer.tick],
207
  fn=lambda: gr.Dropdown(info=fns.get_project_info()),
208
  outputs=[project_dd],
209
  show_progress="hidden",
210
- api_name=False,
211
  )
212
  gr.on(
213
  [project_dd.change],
@@ -215,14 +215,14 @@ with gr.Blocks() as run_page:
215
  inputs=[project_dd],
216
  outputs=[runs_table],
217
  show_progress="hidden",
218
- api_name=False,
219
  queue=False,
220
  ).then(
221
  fns.update_navbar_value,
222
  inputs=[project_dd],
223
  outputs=[navbar],
224
  show_progress="hidden",
225
- api_name=False,
226
  queue=False,
227
  )
228
 
@@ -232,7 +232,7 @@ with gr.Blocks() as run_page:
232
  inputs=[],
233
  outputs=[delete_run_btn, runs_table, allow_deleting_runs],
234
  show_progress="hidden",
235
- api_name=False,
236
  queue=False,
237
  )
238
  gr.on(
@@ -241,7 +241,7 @@ with gr.Blocks() as run_page:
241
  inputs=[allow_deleting_runs, runs_table],
242
  outputs=[delete_run_btn],
243
  show_progress="hidden",
244
- api_name=False,
245
  queue=False,
246
  )
247
  gr.on(
@@ -254,7 +254,7 @@ with gr.Blocks() as run_page:
254
  inputs=None,
255
  outputs=[delete_run_btn, confirm_btn, cancel_btn],
256
  show_progress="hidden",
257
- api_name=False,
258
  queue=False,
259
  )
260
  gr.on(
@@ -267,7 +267,7 @@ with gr.Blocks() as run_page:
267
  inputs=None,
268
  outputs=[delete_run_btn, confirm_btn, cancel_btn],
269
  show_progress="hidden",
270
- api_name=False,
271
  queue=False,
272
  )
273
  gr.on(
@@ -276,6 +276,6 @@ with gr.Blocks() as run_page:
276
  inputs=[allow_deleting_runs, runs_table, project_dd],
277
  outputs=[runs_table],
278
  show_progress="hidden",
279
- api_name=False,
280
  queue=False,
281
  )
 
200
  outputs=project_dd,
201
  show_progress="hidden",
202
  queue=False,
203
+ api_visibility="private",
204
  )
205
  gr.on(
206
  [timer.tick],
207
  fn=lambda: gr.Dropdown(info=fns.get_project_info()),
208
  outputs=[project_dd],
209
  show_progress="hidden",
210
+ api_visibility="private",
211
  )
212
  gr.on(
213
  [project_dd.change],
 
215
  inputs=[project_dd],
216
  outputs=[runs_table],
217
  show_progress="hidden",
218
+ api_visibility="private",
219
  queue=False,
220
  ).then(
221
  fns.update_navbar_value,
222
  inputs=[project_dd],
223
  outputs=[navbar],
224
  show_progress="hidden",
225
+ api_visibility="private",
226
  queue=False,
227
  )
228
 
 
232
  inputs=[],
233
  outputs=[delete_run_btn, runs_table, allow_deleting_runs],
234
  show_progress="hidden",
235
+ api_visibility="private",
236
  queue=False,
237
  )
238
  gr.on(
 
241
  inputs=[allow_deleting_runs, runs_table],
242
  outputs=[delete_run_btn],
243
  show_progress="hidden",
244
+ api_visibility="private",
245
  queue=False,
246
  )
247
  gr.on(
 
254
  inputs=None,
255
  outputs=[delete_run_btn, confirm_btn, cancel_btn],
256
  show_progress="hidden",
257
+ api_visibility="private",
258
  queue=False,
259
  )
260
  gr.on(
 
267
  inputs=None,
268
  outputs=[delete_run_btn, confirm_btn, cancel_btn],
269
  show_progress="hidden",
270
+ api_visibility="private",
271
  queue=False,
272
  )
273
  gr.on(
 
276
  inputs=[allow_deleting_runs, runs_table, project_dd],
277
  outputs=[runs_table],
278
  show_progress="hidden",
279
+ api_visibility="private",
280
  queue=False,
281
  )
utils.py CHANGED
@@ -3,6 +3,7 @@ import os
3
  import re
4
  import time
5
  from datetime import datetime, timezone
 
6
  from pathlib import Path
7
  from typing import TYPE_CHECKING
8
 
@@ -144,7 +145,7 @@ def generate_readable_name(used_names: list[str], space_id: str | None = None) -
144
  If space_id is provided, generates username-timestamp format instead.
145
  """
146
  if space_id is not None:
147
- username = huggingface_hub.whoami()["name"]
148
  timestamp = int(time.time())
149
  return f"{username}-{timestamp}"
150
  adjectives = [
@@ -418,10 +419,10 @@ def preprocess_space_and_dataset_ids(
418
  space_id: str | None, dataset_id: str | None
419
  ) -> tuple[str | None, str | None]:
420
  if space_id is not None and "/" not in space_id:
421
- username = huggingface_hub.whoami()["name"]
422
  space_id = f"{username}/{space_id}"
423
  if dataset_id is not None and "/" not in dataset_id:
424
- username = huggingface_hub.whoami()["name"]
425
  dataset_id = f"{username}/{dataset_id}"
426
  if space_id is not None and dataset_id is None:
427
  dataset_id = f"{space_id}-dataset"
@@ -465,26 +466,39 @@ def format_timestamp(timestamp_str):
465
  return "Unknown"
466
 
467
 
468
- COLOR_PALETTE = [
 
 
469
  "#3B82F6",
470
- "#EF4444",
471
  "#10B981",
472
- "#F59E0B",
473
  "#8B5CF6",
 
 
474
  "#EC4899",
475
  "#06B6D4",
476
- "#84CC16",
477
- "#F97316",
478
- "#6366F1",
479
  ]
480
 
481
 
482
- def get_color_mapping(runs: list[str], smoothing: bool) -> dict[str, str]:
 
 
 
 
 
 
 
 
 
 
483
  """Generate color mapping for runs, with transparency for original data when smoothing is enabled."""
 
 
 
484
  color_map = {}
485
 
486
  for i, run in enumerate(runs):
487
- base_color = COLOR_PALETTE[i % len(COLOR_PALETTE)]
488
 
489
  if smoothing:
490
  color_map[run] = base_color + "4D"
@@ -802,11 +816,15 @@ def deserialize_values(metrics):
802
  return result
803
 
804
 
805
- def get_full_url(base_url: str, project: str | None, write_token: str) -> str:
 
 
806
  params = []
807
  if project:
808
  params.append(f"project={project}")
809
  params.append(f"write_token={write_token}")
 
 
810
  return base_url + "?" + "&".join(params)
811
 
812
 
@@ -852,3 +870,17 @@ def get_space() -> str | None:
852
  def ordered_subset(items: list[str], subset: list[str] | None) -> list[str]:
853
  subset_set = set(subset or [])
854
  return [item for item in items if item in subset_set]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3
  import re
4
  import time
5
  from datetime import datetime, timezone
6
+ from functools import lru_cache
7
  from pathlib import Path
8
  from typing import TYPE_CHECKING
9
 
 
145
  If space_id is provided, generates username-timestamp format instead.
146
  """
147
  if space_id is not None:
148
+ username = _get_default_namespace()
149
  timestamp = int(time.time())
150
  return f"{username}-{timestamp}"
151
  adjectives = [
 
419
  space_id: str | None, dataset_id: str | None
420
  ) -> tuple[str | None, str | None]:
421
  if space_id is not None and "/" not in space_id:
422
+ username = _get_default_namespace()
423
  space_id = f"{username}/{space_id}"
424
  if dataset_id is not None and "/" not in dataset_id:
425
+ username = _get_default_namespace()
426
  dataset_id = f"{username}/{dataset_id}"
427
  if space_id is not None and dataset_id is None:
428
  dataset_id = f"{space_id}-dataset"
 
466
  return "Unknown"
467
 
468
 
469
+ DEFAULT_COLOR_PALETTE = [
470
+ "#A8769B",
471
+ "#E89957",
472
  "#3B82F6",
 
473
  "#10B981",
474
+ "#EF4444",
475
  "#8B5CF6",
476
+ "#14B8A6",
477
+ "#F59E0B",
478
  "#EC4899",
479
  "#06B6D4",
 
 
 
480
  ]
481
 
482
 
483
+ def get_color_palette() -> list[str]:
484
+ """Get the color palette from environment variable or use default."""
485
+ env_palette = os.environ.get("TRACKIO_COLOR_PALETTE")
486
+ if env_palette:
487
+ return [color.strip() for color in env_palette.split(",")]
488
+ return DEFAULT_COLOR_PALETTE
489
+
490
+
491
+ def get_color_mapping(
492
+ runs: list[str], smoothing: bool, color_palette: list[str] | None = None
493
+ ) -> dict[str, str]:
494
  """Generate color mapping for runs, with transparency for original data when smoothing is enabled."""
495
+ if color_palette is None:
496
+ color_palette = get_color_palette()
497
+
498
  color_map = {}
499
 
500
  for i, run in enumerate(runs):
501
+ base_color = color_palette[i % len(color_palette)]
502
 
503
  if smoothing:
504
  color_map[run] = base_color + "4D"
 
816
  return result
817
 
818
 
819
+ def get_full_url(
820
+ base_url: str, project: str | None, write_token: str, footer: bool = True
821
+ ) -> str:
822
  params = []
823
  if project:
824
  params.append(f"project={project}")
825
  params.append(f"write_token={write_token}")
826
+ if not footer:
827
+ params.append("footer=false")
828
  return base_url + "?" + "&".join(params)
829
 
830
 
 
870
  def ordered_subset(items: list[str], subset: list[str] | None) -> list[str]:
871
  subset_set = set(subset or [])
872
  return [item for item in items if item in subset_set]
873
+
874
+
875
+ def _get_default_namespace() -> str:
876
+ """Get the default namespace (username).
877
+
878
+ This function uses caching to avoid repeated API calls to /whoami-v2.
879
+ """
880
+ token = huggingface_hub.get_token()
881
+ return _cached_whoami(token)["name"]
882
+
883
+
884
+ @lru_cache(maxsize=32)
885
+ def _cached_whoami(token: str | None) -> dict:
886
+ return huggingface_hub.whoami(token=token)