WalisonCruz/function-gemma
0
1import math2import os3import re4import secrets5import time6from datetime import datetime, timezone7from functools import lru_cache8from pathlib import Path9from typing import TYPE_CHECKING10 11import huggingface_hub12import numpy as np13import pandas as pd14from huggingface_hub.constants import HF_HOME15 16if TYPE_CHECKING:17 from trackio.commit_scheduler import CommitScheduler18 from trackio.dummy_commit_scheduler import DummyCommitScheduler19 20RESERVED_KEYS = ["project", "run", "timestamp", "step", "time", "metrics"]21 22TRACKIO_LOGO_DIR = Path(__file__).parent / "assets"23 24 25def get_logo_urls() -> dict[str, str]:26 """Get logo URLs from environment variables or use defaults."""27 light_url = os.environ.get(28 "TRACKIO_LOGO_LIGHT_URL",29 f"/gradio_api/file={TRACKIO_LOGO_DIR}/trackio_logo_type_light_transparent.png",30 )31 dark_url = os.environ.get(32 "TRACKIO_LOGO_DARK_URL",33 f"/gradio_api/file={TRACKIO_LOGO_DIR}/trackio_logo_type_dark_transparent.png",34 )35 return {"light": light_url, "dark": dark_url}36 37 38def order_metrics_by_plot_preference(metrics: list[str]) -> tuple[list[str], dict]:39 """40 Order metrics based on TRACKIO_PLOT_ORDER environment variable and group them.41 42 Args:43 metrics: List of metric names to order and group44 45 Returns:46 Tuple of (ordered_group_names, grouped_metrics_dict)47 """48 plot_order_env = os.environ.get("TRACKIO_PLOT_ORDER", "")49 if not plot_order_env.strip():50 plot_order = []51 else:52 plot_order = [53 item.strip() for item in plot_order_env.split(",") if item.strip()54 ]55 56 def get_metric_priority(metric: str) -> tuple[int, int, str]:57 if not plot_order:58 return (float("inf"), float("inf"), metric)59 60 group_prefix = metric.split("/")[0] if "/" in metric else "charts"61 no_match_priority = len(plot_order)62 63 group_priority = no_match_priority64 for i, pattern in enumerate(plot_order):65 pattern_group = pattern.split("/")[0] if "/" in pattern else "charts"66 if pattern_group == group_prefix:67 group_priority = i68 break69 70 within_group_priority = no_match_priority71 for i, pattern in enumerate(plot_order):72 if pattern == metric:73 within_group_priority = i74 break75 elif pattern.endswith("/*") and within_group_priority == no_match_priority:76 pattern_prefix = pattern[:-2]77 if metric.startswith(pattern_prefix + "/"):78 within_group_priority = i + len(plot_order)79 80 return (group_priority, within_group_priority, metric)81 82 result = {}83 for metric in metrics:84 if "/" not in metric:85 if "charts" not in result:86 result["charts"] = {"direct_metrics": [], "subgroups": {}}87 result["charts"]["direct_metrics"].append(metric)88 else:89 parts = metric.split("/")90 main_prefix = parts[0]91 if main_prefix not in result:92 result[main_prefix] = {"direct_metrics": [], "subgroups": {}}93 if len(parts) == 2:94 result[main_prefix]["direct_metrics"].append(metric)95 else:96 subprefix = parts[1]97 if subprefix not in result[main_prefix]["subgroups"]:98 result[main_prefix]["subgroups"][subprefix] = []99 result[main_prefix]["subgroups"][subprefix].append(metric)100 101 for group_data in result.values():102 group_data["direct_metrics"].sort(key=get_metric_priority)103 for subgroup_name in group_data["subgroups"]:104 group_data["subgroups"][subgroup_name].sort(key=get_metric_priority)105 106 if "charts" in result and not result["charts"]["direct_metrics"]:107 del result["charts"]108 109 def get_group_priority(group_name: str) -> tuple[int, str]:110 if not plot_order:111 return (float("inf"), group_name)112 113 min_priority = len(plot_order)114 for i, pattern in enumerate(plot_order):115 pattern_group = pattern.split("/")[0] if "/" in pattern else "charts"116 if pattern_group == group_name:117 min_priority = min(min_priority, i)118 return (min_priority, group_name)119 120 ordered_groups = sorted(result.keys(), key=get_group_priority)121 122 return ordered_groups, result123 124 125def persistent_storage_enabled() -> bool:126 return (127 os.environ.get("PERSISTANT_STORAGE_ENABLED") == "true"128 ) # typo in the name of the environment variable129 130 131def _get_trackio_dir() -> Path:132 if persistent_storage_enabled():133 return Path("/data/trackio")134 elif os.environ.get("TRACKIO_DIR"):135 return Path(os.environ.get("TRACKIO_DIR"))136 return Path(HF_HOME) / "trackio"137 138 139TRACKIO_DIR = _get_trackio_dir()140MEDIA_DIR = TRACKIO_DIR / "media"141FILES_DIR = TRACKIO_DIR / "files"142 143 144def get_or_create_project_hash(project: str) -> str:145 hash_path = TRACKIO_DIR / f"{project}.hash"146 if hash_path.exists():147 return hash_path.read_text().strip()148 hash_value = secrets.token_urlsafe(8)149 TRACKIO_DIR.mkdir(parents=True, exist_ok=True)150 hash_path.write_text(hash_value)151 return hash_value152 153 154def generate_readable_name(used_names: list[str], space_id: str | None = None) -> str:155 """156 Generates a random, readable name like "dainty-sunset-0".157 If space_id is provided, generates username-timestamp format instead.158 """159 if space_id is not None:160 username = _get_default_namespace()161 timestamp = int(time.time())162 return f"{username}-{timestamp}"163 adjectives = [164 "dainty",165 "brave",166 "calm",167 "eager",168 "fancy",169 "gentle",170 "happy",171 "jolly",172 "kind",173 "lively",174 "merry",175 "nice",176 "proud",177 "quick",178 "hugging",179 "silly",180 "tidy",181 "witty",182 "zealous",183 "bright",184 "shy",185 "bold",186 "clever",187 "daring",188 "elegant",189 "faithful",190 "graceful",191 "honest",192 "inventive",193 "jovial",194 "keen",195 "lucky",196 "modest",197 "noble",198 "optimistic",199 "patient",200 "quirky",201 "resourceful",202 "sincere",203 "thoughtful",204 "upbeat",205 "valiant",206 "warm",207 "youthful",208 "zesty",209 "adventurous",210 "breezy",211 "cheerful",212 "delightful",213 "energetic",214 "fearless",215 "glad",216 "hopeful",217 "imaginative",218 "joyful",219 "kindly",220 "luminous",221 "mysterious",222 "neat",223 "outgoing",224 "playful",225 "radiant",226 "spirited",227 "tranquil",228 "unique",229 "vivid",230 "wise",231 "zany",232 "artful",233 "bubbly",234 "charming",235 "dazzling",236 "earnest",237 "festive",238 "gentlemanly",239 "hearty",240 "intrepid",241 "jubilant",242 "knightly",243 "lively",244 "magnetic",245 "nimble",246 "orderly",247 "peaceful",248 "quick-witted",249 "robust",250 "sturdy",251 "trusty",252 "upstanding",253 "vibrant",254 "whimsical",255 ]256 nouns = [257 "sunset",258 "forest",259 "river",260 "mountain",261 "breeze",262 "meadow",263 "ocean",264 "valley",265 "sky",266 "field",267 "cloud",268 "star",269 "rain",270 "leaf",271 "stone",272 "flower",273 "bird",274 "tree",275 "wave",276 "trail",277 "island",278 "desert",279 "hill",280 "lake",281 "pond",282 "grove",283 "canyon",284 "reef",285 "bay",286 "peak",287 "glade",288 "marsh",289 "cliff",290 "dune",291 "spring",292 "brook",293 "cave",294 "plain",295 "ridge",296 "wood",297 "blossom",298 "petal",299 "root",300 "branch",301 "seed",302 "acorn",303 "pine",304 "willow",305 "cedar",306 "elm",307 "falcon",308 "eagle",309 "sparrow",310 "robin",311 "owl",312 "finch",313 "heron",314 "crane",315 "duck",316 "swan",317 "fox",318 "wolf",319 "bear",320 "deer",321 "moose",322 "otter",323 "beaver",324 "lynx",325 "hare",326 "badger",327 "butterfly",328 "bee",329 "ant",330 "beetle",331 "dragonfly",332 "firefly",333 "ladybug",334 "moth",335 "spider",336 "worm",337 "coral",338 "kelp",339 "shell",340 "pebble",341 "face",342 "boulder",343 "cobble",344 "sand",345 "wavelet",346 "tide",347 "current",348 "mist",349 ]350 number = 0351 name = f"{adjectives[0]}-{nouns[0]}-{number}"352 while name in used_names:353 number += 1354 adjective = adjectives[number % len(adjectives)]355 noun = nouns[number % len(nouns)]356 name = f"{adjective}-{noun}-{number}"357 return name358 359 360def is_in_notebook():361 """362 Detect if code is running in a notebook environment (Jupyter, Colab, etc.).363 """364 try:365 from IPython import get_ipython366 367 if get_ipython() is not None:368 return get_ipython().__class__.__name__ in [369 "ZMQInteractiveShell", # Jupyter notebook/lab370 "Shell", # IPython terminal371 ] or "google.colab" in str(get_ipython())372 except ImportError:373 pass374 return False375 376 377def block_main_thread_until_keyboard_interrupt():378 try:379 while True:380 time.sleep(0.1)381 except (KeyboardInterrupt, OSError):382 print("Keyboard interruption in main thread... closing dashboard.")383 384 385def simplify_column_names(columns: list[str]) -> dict[str, str]:386 """387 Simplifies column names to first 10 alphanumeric or "/" characters with unique suffixes.388 389 Args:390 columns: List of original column names391 392 Returns:393 Dictionary mapping original column names to simplified names394 """395 simplified_names = {}396 used_names = set()397 398 for col in columns:399 alphanumeric = re.sub(r"[^a-zA-Z0-9/]", "", col)400 base_name = alphanumeric[:10] if alphanumeric else f"col_{len(used_names)}"401 402 final_name = base_name403 suffix = 1404 while final_name in used_names:405 final_name = f"{base_name}_{suffix}"406 suffix += 1407 408 simplified_names[col] = final_name409 used_names.add(final_name)410 411 return simplified_names412 413 414def print_dashboard_instructions(project: str) -> None:415 """416 Prints instructions for viewing the Trackio dashboard.417 418 Args:419 project: The name of the project to show dashboard for.420 """421 ORANGE = "\033[38;5;208m"422 BOLD = "\033[1m"423 RESET = "\033[0m"424 425 print("* View dashboard by running in your terminal:")426 print(f'{BOLD}{ORANGE}trackio show --project "{project}"{RESET}')427 print(f'* or by running in Python: trackio.show(project="{project}")')428 429 430def preprocess_space_and_dataset_ids(431 space_id: str | None, dataset_id: str | None432) -> tuple[str | None, str | None]:433 """434 Preprocesses the Space and Dataset names to ensure they are valid "username/space_id" or "username/dataset_id" format.435 """436 if space_id is not None and "/" not in space_id:437 username = _get_default_namespace()438 space_id = f"{username}/{space_id}"439 if dataset_id is not None and "/" not in dataset_id:440 username = _get_default_namespace()441 dataset_id = f"{username}/{dataset_id}"442 if space_id is not None and dataset_id is None:443 dataset_id = f"{space_id}-dataset"444 return space_id, dataset_id445 446 447def fibo():448 """Generator for Fibonacci backoff: 1, 1, 2, 3, 5, 8, ..."""449 a, b = 1, 1450 while True:451 yield a452 a, b = b, a + b453 454 455def format_timestamp(timestamp_str):456 """Convert ISO timestamp to human-readable format like '3 minutes ago'."""457 if not timestamp_str or pd.isna(timestamp_str):458 return "Unknown"459 460 try:461 created_time = datetime.fromisoformat(timestamp_str.replace("Z", "+00:00"))462 if created_time.tzinfo is None:463 created_time = created_time.replace(tzinfo=timezone.utc)464 465 now = datetime.now(timezone.utc)466 diff = now - created_time467 468 seconds = int(diff.total_seconds())469 if seconds < 60:470 return "Just now"471 elif seconds < 3600:472 minutes = seconds // 60473 return f"{minutes} minute{'s' if minutes != 1 else ''} ago"474 elif seconds < 86400:475 hours = seconds // 3600476 return f"{hours} hour{'s' if hours != 1 else ''} ago"477 else:478 days = seconds // 86400479 return f"{days} day{'s' if days != 1 else ''} ago"480 except Exception:481 return "Unknown"482 483 484DEFAULT_COLOR_PALETTE = [485 "#A8769B",486 "#E89957",487 "#3B82F6",488 "#10B981",489 "#EF4444",490 "#8B5CF6",491 "#14B8A6",492 "#F59E0B",493 "#EC4899",494 "#06B6D4",495]496 497 498def get_color_palette() -> list[str]:499 """Get the color palette from environment variable or use default."""500 env_palette = os.environ.get("TRACKIO_COLOR_PALETTE")501 if env_palette:502 return [color.strip() for color in env_palette.split(",")]503 return DEFAULT_COLOR_PALETTE504 505 506def get_color_mapping(507 runs: list[str], smoothing: bool, color_palette: list[str] | None = None508) -> dict[str, str]:509 """Generate color mapping for runs, with transparency for original data when smoothing is enabled."""510 if color_palette is None:511 color_palette = get_color_palette()512 513 color_map = {}514 515 for i, run in enumerate(runs):516 base_color = color_palette[i % len(color_palette)]517 518 if smoothing:519 color_map[run] = base_color + "4D"520 color_map[f"{run}_smoothed"] = base_color521 else:522 color_map[run] = base_color523 524 return color_map525 526 527def downsample(528 df: pd.DataFrame,529 x: str,530 y: str,531 color: str | None,532 x_lim: tuple[float | None, float | None] | None = None,533) -> tuple[pd.DataFrame, tuple[float, float] | None]:534 """535 Downsample the dataframe to reduce the number of points plotted.536 Also updates the x-axis limits to the data min/max if either of the x-axis limits are None.537 538 Args:539 df: The dataframe to downsample.540 x: The column name to use for the x-axis.541 y: The column name to use for the y-axis.542 color: The column name to use for the color.543 x_lim: The x-axis limits to use.544 545 Returns:546 A tuple containing the downsampled dataframe and the updated x-axis limits.547 """548 if df.empty:549 if x_lim is not None:550 x_lim = (x_lim[0] or 0, x_lim[1] or 0)551 return df, x_lim552 553 columns_to_keep = [x, y]554 if color is not None and color in df.columns:555 columns_to_keep.append(color)556 df = df[columns_to_keep].copy()557 558 data_x_min = df[x].min()559 data_x_max = df[x].max()560 561 if x_lim is not None:562 x_min, x_max = x_lim563 if x_min is None:564 x_min = data_x_min565 if x_max is None:566 x_max = data_x_max567 updated_x_lim = (x_min, x_max)568 else:569 updated_x_lim = None570 571 n_bins = 100572 573 if color is not None and color in df.columns:574 groups = df.groupby(color)575 else:576 groups = [(None, df)]577 578 downsampled_indices = []579 580 for _, group_df in groups:581 if group_df.empty:582 continue583 584 group_df = group_df.sort_values(x)585 586 if updated_x_lim is not None:587 x_min, x_max = updated_x_lim588 before_point = group_df[group_df[x] < x_min].tail(1)589 after_point = group_df[group_df[x] > x_max].head(1)590 group_df = group_df[(group_df[x] >= x_min) & (group_df[x] <= x_max)]591 else:592 before_point = after_point = None593 x_min = group_df[x].min()594 x_max = group_df[x].max()595 596 if before_point is not None and not before_point.empty:597 downsampled_indices.extend(before_point.index.tolist())598 if after_point is not None and not after_point.empty:599 downsampled_indices.extend(after_point.index.tolist())600 601 if group_df.empty:602 continue603 604 if x_min == x_max:605 min_y_idx = group_df[y].idxmin()606 max_y_idx = group_df[y].idxmax()607 if min_y_idx != max_y_idx:608 downsampled_indices.extend([min_y_idx, max_y_idx])609 else:610 downsampled_indices.append(min_y_idx)611 continue612 613 if len(group_df) < 500:614 downsampled_indices.extend(group_df.index.tolist())615 continue616 617 bins = np.linspace(x_min, x_max, n_bins + 1)618 group_df["bin"] = pd.cut(619 group_df[x], bins=bins, labels=False, include_lowest=True620 )621 622 for bin_idx in group_df["bin"].dropna().unique():623 bin_data = group_df[group_df["bin"] == bin_idx]624 if bin_data.empty:625 continue626 627 min_y_idx = bin_data[y].idxmin()628 max_y_idx = bin_data[y].idxmax()629 630 downsampled_indices.append(min_y_idx)631 if min_y_idx != max_y_idx:632 downsampled_indices.append(max_y_idx)633 634 unique_indices = list(set(downsampled_indices))635 636 downsampled_df = df.loc[unique_indices].copy()637 638 if color is not None:639 downsampled_df = (640 downsampled_df.groupby(color, sort=False)[downsampled_df.columns]641 .apply(lambda group: group.sort_values(x))642 .reset_index(drop=True)643 )644 else:645 downsampled_df = downsampled_df.sort_values(x).reset_index(drop=True)646 647 downsampled_df = downsampled_df.drop(columns=["bin"], errors="ignore")648 649 return downsampled_df, updated_x_lim650 651 652def sort_metrics_by_prefix(metrics: list[str]) -> list[str]:653 """654 Sort metrics by grouping prefixes together for dropdown/list display.655 Metrics without prefixes come first, then grouped by prefix.656 657 Args:658 metrics: List of metric names659 660 Returns:661 List of metric names sorted by prefix662 663 Example:664 Input: ["train/loss", "loss", "train/acc", "val/loss"]665 Output: ["loss", "train/acc", "train/loss", "val/loss"]666 """667 groups = group_metrics_by_prefix(metrics)668 result = []669 670 if "charts" in groups:671 result.extend(groups["charts"])672 673 for group_name in sorted(groups.keys()):674 if group_name != "charts":675 result.extend(groups[group_name])676 677 return result678 679 680def group_metrics_by_prefix(metrics: list[str]) -> dict[str, list[str]]:681 """682 Group metrics by their prefix. Metrics without prefix go to 'charts' group.683 684 Args:685 metrics: List of metric names686 687 Returns:688 Dictionary with prefix names as keys and lists of metrics as values689 690 Example:691 Input: ["loss", "accuracy", "train/loss", "train/acc", "val/loss"]692 Output: {693 "charts": ["loss", "accuracy"],694 "train": ["train/loss", "train/acc"],695 "val": ["val/loss"]696 }697 """698 no_prefix = []699 with_prefix = []700 701 for metric in metrics:702 if "/" in metric:703 with_prefix.append(metric)704 else:705 no_prefix.append(metric)706 707 no_prefix.sort()708 709 prefix_groups = {}710 for metric in with_prefix:711 prefix = metric.split("/")[0]712 if prefix not in prefix_groups:713 prefix_groups[prefix] = []714 prefix_groups[prefix].append(metric)715 716 for prefix in prefix_groups:717 prefix_groups[prefix].sort()718 719 groups = {}720 if no_prefix:721 groups["charts"] = no_prefix722 723 for prefix in sorted(prefix_groups.keys()):724 groups[prefix] = prefix_groups[prefix]725 726 return groups727 728 729def get_sync_status(scheduler: "CommitScheduler | DummyCommitScheduler") -> int | None:730 """Get the sync status from the CommitScheduler in an integer number of minutes, or None if not synced yet."""731 if getattr(732 scheduler, "last_push_time", None733 ): # DummyCommitScheduler doesn't have last_push_time734 time_diff = time.time() - scheduler.last_push_time735 return int(time_diff / 60)736 else:737 return None738 739 740def generate_embed_code(project: str, metrics: str, selected_runs: list = None) -> str:741 """Generate the embed iframe code based on current settings."""742 space_host = os.environ.get("SPACE_HOST", "")743 if not space_host:744 return ""745 746 params = []747 748 if project:749 params.append(f"project={project}")750 751 if metrics and metrics.strip():752 params.append(f"metrics={metrics}")753 754 if selected_runs:755 runs_param = ",".join(selected_runs)756 params.append(f"runs={runs_param}")757 758 params.append("sidebar=hidden")759 params.append("navbar=hidden")760 761 query_string = "&".join(params)762 embed_url = f"https://{space_host}?{query_string}"763 764 return f'<iframe src="{embed_url}" style="width:1600px; height:500px; border:0;"></iframe>'765 766 767def serialize_values(metrics):768 """769 Serialize infinity and NaN values in metrics dict to make it JSON-compliant.770 Only handles top-level float values.771 772 Converts:773 - float('inf') -> "Infinity"774 - float('-inf') -> "-Infinity"775 - float('nan') -> "NaN"776 777 Example:778 {"loss": float('inf'), "accuracy": 0.95} -> {"loss": "Infinity", "accuracy": 0.95}779 """780 if not isinstance(metrics, dict):781 return metrics782 783 result = {}784 for key, value in metrics.items():785 if isinstance(value, float):786 if math.isinf(value):787 result[key] = "Infinity" if value > 0 else "-Infinity"788 elif math.isnan(value):789 result[key] = "NaN"790 else:791 result[key] = value792 elif isinstance(value, np.floating):793 float_val = float(value)794 if math.isinf(float_val):795 result[key] = "Infinity" if float_val > 0 else "-Infinity"796 elif math.isnan(float_val):797 result[key] = "NaN"798 else:799 result[key] = float_val800 else:801 result[key] = value802 return result803 804 805def deserialize_values(metrics):806 """807 Deserialize infinity and NaN string values back to their numeric forms.808 Only handles top-level string values.809 810 Converts:811 - "Infinity" -> float('inf')812 - "-Infinity" -> float('-inf')813 - "NaN" -> float('nan')814 815 Example:816 {"loss": "Infinity", "accuracy": 0.95} -> {"loss": float('inf'), "accuracy": 0.95}817 """818 if not isinstance(metrics, dict):819 return metrics820 821 result = {}822 for key, value in metrics.items():823 if value == "Infinity":824 result[key] = float("inf")825 elif value == "-Infinity":826 result[key] = float("-inf")827 elif value == "NaN":828 result[key] = float("nan")829 else:830 result[key] = value831 return result832 833 834def get_full_url(835 base_url: str, project: str | None, write_token: str, footer: bool = True836) -> str:837 params = []838 if project:839 params.append(f"project={project}")840 params.append(f"write_token={write_token}")841 if not footer:842 params.append("footer=false")843 return base_url + "?" + "&".join(params)844 845 846def embed_url_in_notebook(url: str) -> None:847 try:848 from IPython.display import HTML, display849 850 embed_code = HTML(851 f'<div><iframe src="{url}" width="100%" height="1000px" allow="autoplay; camera; microphone; clipboard-read; clipboard-write;" frameborder="0" allowfullscreen></iframe></div>'852 )853 display(embed_code)854 except ImportError:855 pass856 857 858def to_json_safe(obj):859 if isinstance(obj, (str, int, float, bool, type(None))):860 return obj861 if isinstance(obj, np.generic):862 return obj.item()863 if isinstance(obj, dict):864 return {str(k): to_json_safe(v) for k, v in obj.items()}865 if isinstance(obj, (list, tuple, set)):866 return [to_json_safe(v) for v in obj]867 if hasattr(obj, "to_dict") and callable(obj.to_dict):868 return to_json_safe(obj.to_dict())869 if hasattr(obj, "__dict__"):870 return {871 str(k): to_json_safe(v)872 for k, v in vars(obj).items()873 if not k.startswith("_")874 }875 return str(obj)876 877 878def get_space() -> str | None:879 """880 Get the space ID ("user/space") if Trackio is running in a Space, or None if not.881 """882 return os.environ.get("SPACE_ID")883 884 885def ordered_subset(items: list[str], subset: list[str] | None) -> list[str]:886 subset_set = set(subset or [])887 return [item for item in items if item in subset_set]888 889 890def _get_default_namespace() -> str:891 """Get the default namespace (username).892 893 This function uses caching to avoid repeated API calls to /whoami-v2.894 """895 token = huggingface_hub.get_token()896 return _cached_whoami(token)["name"]897 898 899@lru_cache(maxsize=32)900def _cached_whoami(token: str | None) -> dict:901 return huggingface_hub.whoami(token=token)902 