abidlabs HF Staff commited on
Commit
b83f48f
·
verified ·
1 Parent(s): 983c7aa

Upload folder using huggingface_hub

Browse files
.gitattributes CHANGED
@@ -33,3 +33,4 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
 
 
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
36
+ trackio_logo.png filter=lfs diff=lfs merge=lfs -text
__init__.py ADDED
@@ -0,0 +1,167 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import contextvars
2
+ import time
3
+ import webbrowser
4
+ from pathlib import Path
5
+
6
+ import huggingface_hub
7
+ from gradio_client import Client
8
+ from httpx import ReadTimeout
9
+ from huggingface_hub.errors import RepositoryNotFoundError
10
+
11
+ from trackio.deploy import deploy_as_space
12
+ from trackio.run import Run
13
+ from trackio.ui import demo
14
+ from trackio.utils import TRACKIO_DIR, TRACKIO_LOGO_PATH, block_except_in_notebook
15
+
16
+ __version__ = Path(__file__).parent.joinpath("version.txt").read_text().strip()
17
+
18
+
19
+ current_run: contextvars.ContextVar[Run | None] = contextvars.ContextVar(
20
+ "current_run", default=None
21
+ )
22
+ current_project: contextvars.ContextVar[str | None] = contextvars.ContextVar(
23
+ "current_project", default=None
24
+ )
25
+ current_server: contextvars.ContextVar[str | None] = contextvars.ContextVar(
26
+ "current_server", default=None
27
+ )
28
+
29
+ config = {}
30
+ SPACE_URL = "https://huggingface.co/spaces/{space_id}"
31
+
32
+
33
+ def init(
34
+ project: str,
35
+ name: str | None = None,
36
+ space_id: str | None = None,
37
+ dataset_id: str | None = None,
38
+ config: dict | None = None,
39
+ ) -> Run:
40
+ """
41
+ Creates a new Trackio project and returns a Run object.
42
+
43
+ Args:
44
+ project: The name of the project (can be an existing project to continue tracking or a new project to start tracking from scratch).
45
+ name: The name of the run (if not provided, a default name will be generated).
46
+ space_id: If provided, the project will be logged to a Hugging Face Space instead of a local directory. Should be a complete Space name like "username/reponame". If the Space does not exist, it will be created. If the Space already exists, the project will be logged to it.
47
+ dataset_id: If provided, a persistent Hugging Face Dataset will be created and the metrics will be synced to it every 5 minutes. Should be a complete Dataset name like "username/datasetname". If the Dataset does not exist, it will be created. If the Dataset already exists, the project will be appended to it.
48
+ config: A dictionary of configuration options. Provided for compatibility with wandb.init()
49
+ """
50
+ if not current_server.get() and space_id is None:
51
+ _, url, _ = demo.launch(
52
+ show_api=False, inline=False, quiet=True, prevent_thread_lock=True
53
+ )
54
+ current_server.set(url)
55
+ else:
56
+ url = current_server.get()
57
+
58
+ if current_project.get() is None or current_project.get() != project:
59
+ print(f"* Trackio project initialized: {project}")
60
+
61
+ if space_id is None:
62
+ print(f"* Trackio metrics logged to: {TRACKIO_DIR}")
63
+ print(
64
+ f'\n* View dashboard by running in your terminal: trackio show --project "{project}"'
65
+ )
66
+ print(f'* or by running in Python: trackio.show(project="{project}")')
67
+ else:
68
+ create_space_if_not_exists(space_id, dataset_id)
69
+ print(
70
+ f"* View dashboard by going to: {SPACE_URL.format(space_id=space_id)}"
71
+ )
72
+ current_project.set(project)
73
+
74
+ space_or_url = space_id if space_id else url
75
+ client = Client(space_or_url, verbose=False)
76
+ run = Run(
77
+ project=project, client=client, name=name, config=config, dataset_id=dataset_id
78
+ )
79
+ current_run.set(run)
80
+ globals()["config"] = run.config
81
+ return run
82
+
83
+
84
+ def create_space_if_not_exists(
85
+ space_id: str,
86
+ dataset_id: str | None = None,
87
+ ) -> None:
88
+ """
89
+ Creates a new Hugging Face Space if it does not exist.
90
+
91
+ Args:
92
+ space_id: The ID of the Space to create.
93
+ dataset_id: The ID of the Dataset to create.
94
+ """
95
+ if "/" not in space_id:
96
+ raise ValueError(
97
+ f"Invalid space ID: {space_id}. Must be in the format: username/reponame."
98
+ )
99
+ if dataset_id is not None and "/" not in dataset_id:
100
+ raise ValueError(
101
+ f"Invalid dataset ID: {dataset_id}. Must be in the format: username/datasetname."
102
+ )
103
+ try:
104
+ huggingface_hub.repo_info(space_id, repo_type="space")
105
+ print(f"* Found existing space: {SPACE_URL.format(space_id=space_id)}")
106
+ return
107
+ except RepositoryNotFoundError:
108
+ pass
109
+
110
+ print(f"* Creating new space: {SPACE_URL.format(space_id=space_id)}")
111
+ deploy_as_space(space_id, dataset_id)
112
+
113
+ client = None
114
+ for _ in range(30):
115
+ try:
116
+ client = Client(space_id, verbose=False)
117
+ if client:
118
+ break
119
+ except ReadTimeout:
120
+ print("* Space is not yet ready. Waiting 5 seconds...")
121
+ time.sleep(5)
122
+ except ValueError as e:
123
+ print(f"* Space gave error {e}. Trying again in 5 seconds...")
124
+ time.sleep(5)
125
+
126
+
127
+ def log(metrics: dict) -> None:
128
+ """
129
+ Logs metrics to the current run.
130
+
131
+ Args:
132
+ metrics: A dictionary of metrics to log.
133
+ """
134
+ if current_run.get() is None:
135
+ raise RuntimeError("Call trackio.init() before log().")
136
+ current_run.get().log(metrics)
137
+
138
+
139
+ def finish():
140
+ """
141
+ Finishes the current run.
142
+ """
143
+ if current_run.get() is None:
144
+ raise RuntimeError("Call trackio.init() before finish().")
145
+ current_run.get().finish()
146
+
147
+
148
+ def show(project: str | None = None):
149
+ """
150
+ Launches the Trackio dashboard.
151
+
152
+ Args:
153
+ project: The name of the project whose runs to show. If not provided, all projects will be shown and the user can select one.
154
+ """
155
+ _, url, share_url = demo.launch(
156
+ show_api=False,
157
+ quiet=True,
158
+ inline=False,
159
+ prevent_thread_lock=True,
160
+ favicon_path=TRACKIO_LOGO_PATH,
161
+ allowed_paths=[TRACKIO_LOGO_PATH],
162
+ )
163
+ base_url = share_url + "/" if share_url else url
164
+ dashboard_url = base_url + f"?project={project}" if project else base_url
165
+ print(f"* Trackio UI launched at: {dashboard_url}")
166
+ webbrowser.open(dashboard_url)
167
+ block_except_in_notebook()
__pycache__/__init__.cpython-310.pyc ADDED
Binary file (4.96 kB). View file
 
__pycache__/__init__.cpython-312.pyc ADDED
Binary file (7.73 kB). View file
 
__pycache__/cli.cpython-312.pyc ADDED
Binary file (1.11 kB). View file
 
__pycache__/context.cpython-312.pyc ADDED
Binary file (440 Bytes). View file
 
__pycache__/deploy.cpython-310.pyc ADDED
Binary file (1.72 kB). View file
 
__pycache__/deploy.cpython-312.pyc ADDED
Binary file (2.72 kB). View file
 
__pycache__/dummy_commit_scheduler.cpython-310.pyc ADDED
Binary file (936 Bytes). View file
 
__pycache__/dummy_commit_scheduler.cpython-312.pyc ADDED
Binary file (1.01 kB). View file
 
__pycache__/run.cpython-310.pyc ADDED
Binary file (1.01 kB). View file
 
__pycache__/run.cpython-312.pyc ADDED
Binary file (1.4 kB). View file
 
__pycache__/sqlite_storage.cpython-310.pyc ADDED
Binary file (5.37 kB). View file
 
__pycache__/sqlite_storage.cpython-312.pyc ADDED
Binary file (10.2 kB). View file
 
__pycache__/storage.cpython-312.pyc ADDED
Binary file (4.6 kB). View file
 
__pycache__/ui.cpython-310.pyc ADDED
Binary file (7.83 kB). View file
 
__pycache__/ui.cpython-312.pyc ADDED
Binary file (12.4 kB). View file
 
__pycache__/utils.cpython-310.pyc ADDED
Binary file (2.62 kB). View file
 
__pycache__/utils.cpython-312.pyc ADDED
Binary file (3.19 kB). View file
 
cli.py ADDED
@@ -0,0 +1,26 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import argparse
2
+
3
+ from trackio import show
4
+
5
+
6
+ def main():
7
+ parser = argparse.ArgumentParser(description="Trackio CLI")
8
+ subparsers = parser.add_subparsers(dest="command")
9
+
10
+ ui_parser = subparsers.add_parser(
11
+ "show", help="Show the Trackio dashboard UI for a project"
12
+ )
13
+ ui_parser.add_argument(
14
+ "--project", required=False, help="Project name to show in the dashboard"
15
+ )
16
+
17
+ args = parser.parse_args()
18
+
19
+ if args.command == "show":
20
+ show(args.project)
21
+ else:
22
+ parser.print_help()
23
+
24
+
25
+ if __name__ == "__main__":
26
+ main()
deploy.py ADDED
@@ -0,0 +1,69 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import io
2
+ import os
3
+ from importlib.resources import files
4
+ from pathlib import Path
5
+
6
+ import gradio
7
+ import huggingface_hub
8
+
9
+
10
+ def deploy_as_space(
11
+ title: str,
12
+ dataset_id: str | None = None,
13
+ ):
14
+ if (
15
+ os.getenv("SYSTEM") == "spaces"
16
+ ): # in case a repo with this function is uploaded to spaces
17
+ return
18
+
19
+ trackio_path = files("trackio")
20
+
21
+ hf_api = huggingface_hub.HfApi()
22
+ whoami = None
23
+ login = False
24
+ try:
25
+ whoami = hf_api.whoami()
26
+ if whoami["auth"]["accessToken"]["role"] != "write":
27
+ login = True
28
+ except OSError:
29
+ login = True
30
+ if login:
31
+ print("Need 'write' access token to create a Spaces repo.")
32
+ huggingface_hub.login(add_to_git_credential=False)
33
+ whoami = hf_api.whoami()
34
+
35
+ space_id = huggingface_hub.create_repo(
36
+ title,
37
+ space_sdk="gradio",
38
+ repo_type="space",
39
+ exist_ok=True,
40
+ ).repo_id
41
+ assert space_id == title # not sure why these would differ
42
+
43
+ with open(Path(trackio_path, "README.md"), "r") as f:
44
+ readme_content = f.read()
45
+ readme_content = readme_content.replace("{GRADIO_VERSION}", gradio.__version__)
46
+ readme_buffer = io.BytesIO(readme_content.encode("utf-8"))
47
+ hf_api.upload_file(
48
+ path_or_fileobj=readme_buffer,
49
+ path_in_repo="README.md",
50
+ repo_id=space_id,
51
+ repo_type="space",
52
+ )
53
+
54
+ huggingface_hub.utils.disable_progress_bars()
55
+ hf_api.upload_folder(
56
+ repo_id=space_id,
57
+ repo_type="space",
58
+ folder_path=trackio_path,
59
+ ignore_patterns=["README.md"],
60
+ )
61
+
62
+ hf_token = huggingface_hub.utils.get_token()
63
+ if hf_token is not None:
64
+ huggingface_hub.add_space_secret(space_id, "HF_TOKEN", hf_token)
65
+ if dataset_id is not None:
66
+ huggingface_hub.add_space_variable(space_id, "TRACKIO_DATASET_ID", dataset_id)
67
+ # So that the dataset id is available to the sqlite_storage.py file
68
+ # if running locally as well.
69
+ os.environ["TRACKIO_DATASET_ID"] = dataset_id
dummy_commit_scheduler.py ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # A dummy object to fit the interface of huggingface_hub's CommitScheduler
2
+ class DummyCommitSchedulerLock:
3
+ def __enter__(self):
4
+ return None
5
+
6
+ def __exit__(self, exception_type, exception_value, exception_traceback):
7
+ pass
8
+
9
+
10
+ class DummyCommitScheduler:
11
+ def __init__(self):
12
+ self.lock = DummyCommitSchedulerLock()
run.py ADDED
@@ -0,0 +1,31 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from gradio_client import Client
2
+
3
+ from trackio.utils import generate_readable_name
4
+
5
+
6
+ class Run:
7
+ def __init__(
8
+ self,
9
+ project: str,
10
+ client: Client,
11
+ name: str | None = None,
12
+ config: dict | None = None,
13
+ dataset_id: str | None = None,
14
+ ):
15
+ self.project = project
16
+ self.client = client
17
+ self.name = name or generate_readable_name()
18
+ self.config = config or {}
19
+ self.dataset_id = dataset_id
20
+
21
+ def log(self, metrics: dict):
22
+ self.client.predict(
23
+ api_name="/log",
24
+ project=self.project,
25
+ run=self.name,
26
+ metrics=metrics,
27
+ dataset_id=self.dataset_id,
28
+ )
29
+
30
+ def finish(self):
31
+ pass
sqlite_storage.py ADDED
@@ -0,0 +1,191 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import glob
2
+ import json
3
+ import os
4
+ import sqlite3
5
+
6
+ from huggingface_hub import CommitScheduler
7
+
8
+ try:
9
+ from trackio.dummy_commit_scheduler import DummyCommitScheduler
10
+ from trackio.utils import RESERVED_KEYS, TRACKIO_DIR
11
+ except: # noqa: E722
12
+ from dummy_commit_scheduler import DummyCommitScheduler
13
+ from utils import RESERVED_KEYS, TRACKIO_DIR
14
+
15
+
16
+ class SQLiteStorage:
17
+ def __init__(
18
+ self, project: str, name: str, config: dict, dataset_id: str | None = None
19
+ ):
20
+ self.project = project
21
+ self.name = name
22
+ self.config = config
23
+ self.db_path = self._get_project_db_path(project)
24
+ self.dataset_id = dataset_id
25
+ self.scheduler = self._get_scheduler()
26
+
27
+ os.makedirs(TRACKIO_DIR, exist_ok=True)
28
+
29
+ self._init_db()
30
+ self._save_config()
31
+
32
+ @staticmethod
33
+ def _get_project_db_path(project: str) -> str:
34
+ """Get the database path for a specific project."""
35
+ safe_project_name = "".join(
36
+ c for c in project if c.isalnum() or c in ("-", "_")
37
+ ).rstrip()
38
+ if not safe_project_name:
39
+ safe_project_name = "default"
40
+ return os.path.join(TRACKIO_DIR, f"{safe_project_name}.db")
41
+
42
+ def _get_scheduler(self):
43
+ hf_token = os.environ.get(
44
+ "HF_TOKEN"
45
+ ) # Get the token from the environment variable on Spaces
46
+ dataset_id = self.dataset_id or os.environ.get("TRACKIO_DATASET_ID")
47
+ if dataset_id is None:
48
+ scheduler = DummyCommitScheduler()
49
+ else:
50
+ scheduler = CommitScheduler(
51
+ repo_id=dataset_id,
52
+ repo_type="dataset",
53
+ folder_path=TRACKIO_DIR,
54
+ private=True,
55
+ squash_history=True,
56
+ token=hf_token,
57
+ )
58
+ return scheduler
59
+
60
+ def _init_db(self):
61
+ """Initialize the SQLite database with required tables."""
62
+ with self.scheduler.lock:
63
+ with sqlite3.connect(self.db_path) as conn:
64
+ cursor = conn.cursor()
65
+
66
+ cursor.execute("""
67
+ CREATE TABLE IF NOT EXISTS metrics (
68
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
69
+ timestamp DATETIME DEFAULT CURRENT_TIMESTAMP,
70
+ project_name TEXT NOT NULL,
71
+ run_name TEXT NOT NULL,
72
+ metrics TEXT NOT NULL
73
+ )
74
+ """)
75
+
76
+ cursor.execute("""
77
+ CREATE TABLE IF NOT EXISTS configs (
78
+ project_name TEXT NOT NULL,
79
+ run_name TEXT NOT NULL,
80
+ config TEXT NOT NULL,
81
+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
82
+ PRIMARY KEY (project_name, run_name)
83
+ )
84
+ """)
85
+
86
+ conn.commit()
87
+
88
+ def _save_config(self):
89
+ """Save the run configuration to the database."""
90
+ with self.scheduler.lock:
91
+ with sqlite3.connect(self.db_path) as conn:
92
+ cursor = conn.cursor()
93
+ cursor.execute(
94
+ "INSERT OR REPLACE INTO configs (project_name, run_name, config) VALUES (?, ?, ?)",
95
+ (self.project, self.name, json.dumps(self.config)),
96
+ )
97
+ conn.commit()
98
+
99
+ def log(self, metrics: dict):
100
+ """Log metrics to the database."""
101
+ for k in metrics.keys():
102
+ if k in RESERVED_KEYS or k.startswith("__"):
103
+ raise ValueError(
104
+ f"Please do not use this reserved key as a metric: {k}"
105
+ )
106
+
107
+ with self.scheduler.lock:
108
+ with sqlite3.connect(self.db_path) as conn:
109
+ cursor = conn.cursor()
110
+ cursor.execute(
111
+ """
112
+ INSERT INTO metrics
113
+ (project_name, run_name, metrics)
114
+ VALUES (?, ?, ?)
115
+ """,
116
+ (self.project, self.name, json.dumps(metrics)),
117
+ )
118
+ conn.commit()
119
+
120
+ @staticmethod
121
+ def get_metrics(project: str, run: str) -> list[dict]:
122
+ """Retrieve metrics for a specific run."""
123
+ db_path = SQLiteStorage._get_project_db_path(project)
124
+ if not os.path.exists(db_path):
125
+ return []
126
+
127
+ with sqlite3.connect(db_path) as conn:
128
+ cursor = conn.cursor()
129
+ cursor.execute(
130
+ """
131
+ SELECT timestamp, metrics
132
+ FROM metrics
133
+ WHERE project_name = ? AND run_name = ?
134
+ ORDER BY timestamp
135
+ """,
136
+ (project, run),
137
+ )
138
+ rows = cursor.fetchall()
139
+
140
+ results = []
141
+ for row in rows:
142
+ timestamp, metrics_json = row
143
+ metrics = json.loads(metrics_json)
144
+ metrics["timestamp"] = timestamp
145
+ results.append(metrics)
146
+
147
+ return results
148
+
149
+ @staticmethod
150
+ def get_projects() -> list[str]:
151
+ """Get list of all projects by scanning database files."""
152
+ projects = []
153
+ if not os.path.exists(TRACKIO_DIR):
154
+ return projects
155
+
156
+ db_files = glob.glob(os.path.join(TRACKIO_DIR, "*.db"))
157
+
158
+ for db_file in db_files:
159
+ try:
160
+ with sqlite3.connect(db_file) as conn:
161
+ cursor = conn.cursor()
162
+ cursor.execute(
163
+ "SELECT name FROM sqlite_master WHERE type='table' AND name='metrics'"
164
+ )
165
+ if cursor.fetchone():
166
+ cursor.execute("SELECT DISTINCT project_name FROM metrics")
167
+ project_names = [row[0] for row in cursor.fetchall()]
168
+ projects.extend(project_names)
169
+ except sqlite3.Error:
170
+ continue
171
+
172
+ return list(set(projects))
173
+
174
+ @staticmethod
175
+ def get_runs(project: str) -> list[str]:
176
+ """Get list of all runs for a project."""
177
+ db_path = SQLiteStorage._get_project_db_path(project)
178
+ if not os.path.exists(db_path):
179
+ return []
180
+
181
+ with sqlite3.connect(db_path) as conn:
182
+ cursor = conn.cursor()
183
+ cursor.execute(
184
+ "SELECT DISTINCT run_name FROM metrics WHERE project_name = ?",
185
+ (project,),
186
+ )
187
+ return [row[0] for row in cursor.fetchall()]
188
+
189
+ def finish(self):
190
+ """Cleanup when run is finished."""
191
+ pass
trackio_logo.png ADDED

Git LFS Details

  • SHA256: 3922c4d1e465270ad4d8abb12023f3beed5d9f7f338528a4c0ac21dcf358a1c8
  • Pointer size: 131 Bytes
  • Size of remote file: 487 kB
ui.py ADDED
@@ -0,0 +1,327 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ from typing import Any
3
+
4
+ import gradio as gr
5
+ import pandas as pd
6
+
7
+ try:
8
+ from trackio.sqlite_storage import SQLiteStorage
9
+ from trackio.utils import RESERVED_KEYS, TRACKIO_LOGO_PATH
10
+ except: # noqa: E722
11
+ from sqlite_storage import SQLiteStorage
12
+ from utils import RESERVED_KEYS, TRACKIO_LOGO_PATH
13
+
14
+ css = """
15
+ #run-cb .wrap {
16
+ gap: 2px;
17
+ }
18
+ #run-cb .wrap label {
19
+ line-height: 1;
20
+ padding: 6px;
21
+ }
22
+ """
23
+
24
+ COLOR_PALETTE = [
25
+ "#3B82F6",
26
+ "#EF4444",
27
+ "#10B981",
28
+ "#F59E0B",
29
+ "#8B5CF6",
30
+ "#EC4899",
31
+ "#06B6D4",
32
+ "#84CC16",
33
+ "#F97316",
34
+ "#6366F1",
35
+ ]
36
+
37
+
38
+ def get_color_mapping(runs: list[str], smoothing: bool) -> dict[str, str]:
39
+ """Generate color mapping for runs, with transparency for original data when smoothing is enabled."""
40
+ color_map = {}
41
+
42
+ for i, run in enumerate(runs):
43
+ base_color = COLOR_PALETTE[i % len(COLOR_PALETTE)]
44
+
45
+ if smoothing:
46
+ color_map[f"{run}_smoothed"] = base_color
47
+ color_map[f"{run}_original"] = base_color + "4D"
48
+ else:
49
+ color_map[run] = base_color
50
+
51
+ return color_map
52
+
53
+
54
+ def get_projects(request: gr.Request):
55
+ dataset_id = os.environ.get("TRACKIO_DATASET_ID")
56
+ projects = SQLiteStorage.get_projects()
57
+ if project := request.query_params.get("project"):
58
+ interactive = False
59
+ else:
60
+ interactive = True
61
+ project = projects[0] if projects else None
62
+ return gr.Dropdown(
63
+ label="Project",
64
+ choices=projects,
65
+ value=project,
66
+ allow_custom_value=True,
67
+ interactive=interactive,
68
+ info=f"&#x21bb; Synced to <a href='https://huggingface.co/{dataset_id}' target='_blank'>{dataset_id}</a> every 5 min"
69
+ if dataset_id
70
+ else None,
71
+ )
72
+
73
+
74
+ def get_runs(project):
75
+ if not project:
76
+ return []
77
+ return SQLiteStorage.get_runs(project)
78
+
79
+
80
+ def load_run_data(project: str | None, run: str | None, smoothing: bool):
81
+ if not project or not run:
82
+ return None
83
+ metrics = SQLiteStorage.get_metrics(project, run)
84
+ if not metrics:
85
+ return None
86
+ df = pd.DataFrame(metrics)
87
+
88
+ if "step" not in df.columns:
89
+ df["step"] = range(len(df))
90
+
91
+ if smoothing:
92
+ numeric_cols = df.select_dtypes(include="number").columns
93
+ numeric_cols = [c for c in numeric_cols if c not in RESERVED_KEYS]
94
+
95
+ df_original = df.copy()
96
+ df_original["run"] = f"{run}_original"
97
+ df_original["data_type"] = "original"
98
+
99
+ df_smoothed = df.copy()
100
+ df_smoothed[numeric_cols] = df_smoothed[numeric_cols].ewm(alpha=0.1).mean()
101
+ df_smoothed["run"] = f"{run}_smoothed"
102
+ df_smoothed["data_type"] = "smoothed"
103
+
104
+ combined_df = pd.concat([df_original, df_smoothed], ignore_index=True)
105
+ return combined_df
106
+ else:
107
+ df["run"] = run
108
+ df["data_type"] = "original"
109
+ return df
110
+
111
+
112
+ def update_runs(project, filter_text, user_interacted_with_runs=False):
113
+ if project is None:
114
+ runs = []
115
+ num_runs = 0
116
+ else:
117
+ runs = get_runs(project)
118
+ num_runs = len(runs)
119
+ if filter_text:
120
+ runs = [r for r in runs if filter_text in r]
121
+ if not user_interacted_with_runs:
122
+ return gr.CheckboxGroup(
123
+ choices=runs, value=[runs[0]] if runs else []
124
+ ), gr.Textbox(label=f"Runs ({num_runs})")
125
+ else:
126
+ return gr.CheckboxGroup(choices=runs), gr.Textbox(label=f"Runs ({num_runs})")
127
+
128
+
129
+ def filter_runs(project, filter_text):
130
+ runs = get_runs(project)
131
+ runs = [r for r in runs if filter_text in r]
132
+ return gr.CheckboxGroup(choices=runs, value=runs)
133
+
134
+
135
+ def toggle_timer(cb_value):
136
+ if cb_value:
137
+ return gr.Timer(active=True)
138
+ else:
139
+ return gr.Timer(active=False)
140
+
141
+
142
+ def log(project: str, run: str, metrics: dict[str, Any], dataset_id: str) -> None:
143
+ # Note: the type hint for dataset_id should be str | None but gr.api
144
+ # doesn't support that, see: https://github.com/gradio-app/gradio/issues/11175#issuecomment-2920203317
145
+ storage = SQLiteStorage(project, run, {}, dataset_id=dataset_id)
146
+ storage.log(metrics)
147
+
148
+
149
+ def sort_metrics_by_prefix(metrics: list[str]) -> list[str]:
150
+ """
151
+ Sort metrics by grouping prefixes together.
152
+ Metrics without prefixes come first, then grouped by prefix.
153
+
154
+ Example:
155
+ Input: ["train/loss", "loss", "train/acc", "val/loss"]
156
+ Output: ["loss", "train/acc", "train/loss", "val/loss"]
157
+ """
158
+ no_prefix = []
159
+ with_prefix = []
160
+
161
+ for metric in metrics:
162
+ if "/" in metric:
163
+ with_prefix.append(metric)
164
+ else:
165
+ no_prefix.append(metric)
166
+
167
+ no_prefix.sort()
168
+
169
+ prefix_groups = {}
170
+ for metric in with_prefix:
171
+ prefix = metric.split("/")[0]
172
+ if prefix not in prefix_groups:
173
+ prefix_groups[prefix] = []
174
+ prefix_groups[prefix].append(metric)
175
+
176
+ sorted_with_prefix = []
177
+ for prefix in sorted(prefix_groups.keys()):
178
+ sorted_with_prefix.extend(sorted(prefix_groups[prefix]))
179
+
180
+ return no_prefix + sorted_with_prefix
181
+
182
+
183
+ def configure(request: gr.Request):
184
+ if metrics := request.query_params.get("metrics"):
185
+ return metrics.split(",")
186
+ else:
187
+ return []
188
+
189
+
190
+ with gr.Blocks(theme="citrus", title="Trackio Dashboard", css=css) as demo:
191
+ with gr.Sidebar() as sidebar:
192
+ gr.Markdown(
193
+ f"<div style='display: flex; align-items: center; gap: 8px;'><img src='/gradio_api/file={TRACKIO_LOGO_PATH}' width='32' height='32'><span style='font-size: 2em; font-weight: bold;'>Trackio</span></div>"
194
+ )
195
+ project_dd = gr.Dropdown(label="Project")
196
+ run_tb = gr.Textbox(label="Runs", placeholder="Type to filter...")
197
+ run_cb = gr.CheckboxGroup(
198
+ label="Runs", choices=[], interactive=True, elem_id="run-cb"
199
+ )
200
+
201
+ with gr.Sidebar(position="right", open=False) as settings_sidebar:
202
+ gr.Markdown("### ⚙️ Settings")
203
+ realtime_cb = gr.Checkbox(label="Refresh realtime", value=True)
204
+ smoothing_cb = gr.Checkbox(label="Smoothing", value=True)
205
+
206
+ timer = gr.Timer(value=1)
207
+ metrics_subset = gr.State([])
208
+ user_interacted_with_run_cb = gr.State(False)
209
+
210
+ gr.on(
211
+ [demo.load],
212
+ fn=configure,
213
+ outputs=metrics_subset,
214
+ )
215
+ gr.on(
216
+ [demo.load],
217
+ fn=get_projects,
218
+ outputs=project_dd,
219
+ show_progress="hidden",
220
+ )
221
+ gr.on(
222
+ [timer.tick],
223
+ fn=update_runs,
224
+ inputs=[project_dd, run_tb, user_interacted_with_run_cb],
225
+ outputs=[run_cb, run_tb],
226
+ show_progress="hidden",
227
+ )
228
+ gr.on(
229
+ [demo.load, project_dd.change],
230
+ fn=update_runs,
231
+ inputs=[project_dd, run_tb],
232
+ outputs=[run_cb, run_tb],
233
+ show_progress="hidden",
234
+ )
235
+
236
+ realtime_cb.change(
237
+ fn=toggle_timer,
238
+ inputs=realtime_cb,
239
+ outputs=timer,
240
+ api_name="toggle_timer",
241
+ )
242
+ run_cb.input(
243
+ fn=lambda: True,
244
+ outputs=user_interacted_with_run_cb,
245
+ )
246
+ run_tb.input(
247
+ fn=filter_runs,
248
+ inputs=[project_dd, run_tb],
249
+ outputs=run_cb,
250
+ )
251
+
252
+ gr.api(
253
+ fn=log,
254
+ api_name="log",
255
+ )
256
+
257
+ x_lim = gr.State(None)
258
+
259
+ def update_x_lim(select_data: gr.SelectData):
260
+ return select_data.index
261
+
262
+ @gr.render(
263
+ triggers=[
264
+ demo.load,
265
+ run_cb.change,
266
+ timer.tick,
267
+ smoothing_cb.change,
268
+ x_lim.change,
269
+ ],
270
+ inputs=[project_dd, run_cb, smoothing_cb, metrics_subset, x_lim],
271
+ )
272
+ def update_dashboard(project, runs, smoothing, metrics_subset, x_lim_value):
273
+ dfs = []
274
+ original_runs = runs.copy()
275
+
276
+ for run in runs:
277
+ df = load_run_data(project, run, smoothing)
278
+ if df is not None:
279
+ dfs.append(df)
280
+
281
+ if dfs:
282
+ master_df = pd.concat(dfs, ignore_index=True)
283
+ else:
284
+ master_df = pd.DataFrame()
285
+
286
+ if master_df.empty:
287
+ return
288
+
289
+ numeric_cols = master_df.select_dtypes(include="number").columns
290
+ numeric_cols = [
291
+ c for c in numeric_cols if c not in RESERVED_KEYS and c != "step"
292
+ ]
293
+ if metrics_subset:
294
+ numeric_cols = [c for c in numeric_cols if c in metrics_subset]
295
+
296
+ numeric_cols = sort_metrics_by_prefix(list(numeric_cols))
297
+ color_map = get_color_mapping(original_runs, smoothing)
298
+
299
+ with gr.Row(key="row"):
300
+ for metric_idx, metric_name in enumerate(numeric_cols):
301
+ metric_df = master_df.dropna(subset=[metric_name])
302
+ if not metric_df.empty:
303
+ plot = gr.LinePlot(
304
+ metric_df,
305
+ x="step",
306
+ y=metric_name,
307
+ color="run" if "run" in metric_df.columns else None,
308
+ color_map=color_map,
309
+ title=metric_name,
310
+ key=f"plot-{metric_idx}",
311
+ preserved_by_key=None,
312
+ x_lim=x_lim_value,
313
+ y_lim=[
314
+ metric_df[metric_name].min(),
315
+ metric_df[metric_name].max(),
316
+ ],
317
+ show_fullscreen_button=True,
318
+ min_width=400,
319
+ )
320
+ plot.select(update_x_lim, outputs=x_lim, key=f"select-{metric_idx}")
321
+ plot.double_click(
322
+ lambda: None, outputs=x_lim, key=f"double-{metric_idx}"
323
+ )
324
+
325
+
326
+ if __name__ == "__main__":
327
+ demo.launch(allowed_paths=[TRACKIO_LOGO_PATH], show_api=False)
utils.py ADDED
@@ -0,0 +1,217 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import random
3
+ import sys
4
+ import time
5
+ from pathlib import Path
6
+
7
+ from huggingface_hub.constants import HF_HOME
8
+
9
+ RESERVED_KEYS = ["project", "run", "timestamp", "step"]
10
+ TRACKIO_DIR = os.path.join(HF_HOME, "trackio")
11
+
12
+ TRACKIO_LOGO_PATH = str(Path(__file__).parent.joinpath("trackio_logo.png"))
13
+
14
+
15
+ def generate_readable_name():
16
+ """
17
+ Generates a random, readable name like "dainty-sunset-1"
18
+ """
19
+ adjectives = [
20
+ "dainty",
21
+ "brave",
22
+ "calm",
23
+ "eager",
24
+ "fancy",
25
+ "gentle",
26
+ "happy",
27
+ "jolly",
28
+ "kind",
29
+ "lively",
30
+ "merry",
31
+ "nice",
32
+ "proud",
33
+ "quick",
34
+ "silly",
35
+ "tidy",
36
+ "witty",
37
+ "zealous",
38
+ "bright",
39
+ "shy",
40
+ "bold",
41
+ "clever",
42
+ "daring",
43
+ "elegant",
44
+ "faithful",
45
+ "graceful",
46
+ "honest",
47
+ "inventive",
48
+ "jovial",
49
+ "keen",
50
+ "lucky",
51
+ "modest",
52
+ "noble",
53
+ "optimistic",
54
+ "patient",
55
+ "quirky",
56
+ "resourceful",
57
+ "sincere",
58
+ "thoughtful",
59
+ "upbeat",
60
+ "valiant",
61
+ "warm",
62
+ "youthful",
63
+ "zesty",
64
+ "adventurous",
65
+ "breezy",
66
+ "cheerful",
67
+ "delightful",
68
+ "energetic",
69
+ "fearless",
70
+ "glad",
71
+ "hopeful",
72
+ "imaginative",
73
+ "joyful",
74
+ "kindly",
75
+ "luminous",
76
+ "mysterious",
77
+ "neat",
78
+ "outgoing",
79
+ "playful",
80
+ "radiant",
81
+ "spirited",
82
+ "tranquil",
83
+ "unique",
84
+ "vivid",
85
+ "wise",
86
+ "zany",
87
+ "artful",
88
+ "bubbly",
89
+ "charming",
90
+ "dazzling",
91
+ "earnest",
92
+ "festive",
93
+ "gentlemanly",
94
+ "hearty",
95
+ "intrepid",
96
+ "jubilant",
97
+ "knightly",
98
+ "lively",
99
+ "magnetic",
100
+ "nimble",
101
+ "orderly",
102
+ "peaceful",
103
+ "quick-witted",
104
+ "robust",
105
+ "sturdy",
106
+ "trusty",
107
+ "upstanding",
108
+ "vibrant",
109
+ "whimsical",
110
+ ]
111
+ nouns = [
112
+ "sunset",
113
+ "forest",
114
+ "river",
115
+ "mountain",
116
+ "breeze",
117
+ "meadow",
118
+ "ocean",
119
+ "valley",
120
+ "sky",
121
+ "field",
122
+ "cloud",
123
+ "star",
124
+ "rain",
125
+ "leaf",
126
+ "stone",
127
+ "flower",
128
+ "bird",
129
+ "tree",
130
+ "wave",
131
+ "trail",
132
+ "island",
133
+ "desert",
134
+ "hill",
135
+ "lake",
136
+ "pond",
137
+ "grove",
138
+ "canyon",
139
+ "reef",
140
+ "bay",
141
+ "peak",
142
+ "glade",
143
+ "marsh",
144
+ "cliff",
145
+ "dune",
146
+ "spring",
147
+ "brook",
148
+ "cave",
149
+ "plain",
150
+ "ridge",
151
+ "wood",
152
+ "blossom",
153
+ "petal",
154
+ "root",
155
+ "branch",
156
+ "seed",
157
+ "acorn",
158
+ "pine",
159
+ "willow",
160
+ "cedar",
161
+ "elm",
162
+ "falcon",
163
+ "eagle",
164
+ "sparrow",
165
+ "robin",
166
+ "owl",
167
+ "finch",
168
+ "heron",
169
+ "crane",
170
+ "duck",
171
+ "swan",
172
+ "fox",
173
+ "wolf",
174
+ "bear",
175
+ "deer",
176
+ "moose",
177
+ "otter",
178
+ "beaver",
179
+ "lynx",
180
+ "hare",
181
+ "badger",
182
+ "butterfly",
183
+ "bee",
184
+ "ant",
185
+ "beetle",
186
+ "dragonfly",
187
+ "firefly",
188
+ "ladybug",
189
+ "moth",
190
+ "spider",
191
+ "worm",
192
+ "coral",
193
+ "kelp",
194
+ "shell",
195
+ "pebble",
196
+ "boulder",
197
+ "cobble",
198
+ "sand",
199
+ "wavelet",
200
+ "tide",
201
+ "current",
202
+ ]
203
+ adjective = random.choice(adjectives)
204
+ noun = random.choice(nouns)
205
+ number = random.randint(1, 99)
206
+ return f"{adjective}-{noun}-{number}"
207
+
208
+
209
+ def block_except_in_notebook():
210
+ in_notebook = bool(getattr(sys, "ps1", sys.flags.interactive))
211
+ if in_notebook:
212
+ return
213
+ try:
214
+ while True:
215
+ time.sleep(0.1)
216
+ except (KeyboardInterrupt, OSError):
217
+ print("Keyboard interruption in main thread... closing dashboard.")
version.txt ADDED
@@ -0,0 +1 @@
 
 
1
+ 0.0.10